From 885d680a7a2186905d006e57cbb972c8cf7a847b Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 06:46:10 +0100 Subject: [PATCH 01/66] fix(app_configuration): update SwitchListTile title text for role visibility - Change the title text from "enableInArticleAdsForRoleLabel" to "visibleToRoleLabel" - This update improves clarity in the article ad settings form --- lib/app_configuration/widgets/article_ad_settings_form.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app_configuration/widgets/article_ad_settings_form.dart b/lib/app_configuration/widgets/article_ad_settings_form.dart index 03a2bb2b..2fd78f61 100644 --- a/lib/app_configuration/widgets/article_ad_settings_form.dart +++ b/lib/app_configuration/widgets/article_ad_settings_form.dart @@ -202,7 +202,7 @@ class _ArticleAdSettingsFormState extends State return Column( children: [ SwitchListTile( - title: Text(l10n.enableInArticleAdsForRoleLabel(role.l10n(context))), + title: Text(l10n.visibleToRoleLabel(role.l10n(context))), value: roleSlots != null, onChanged: (value) { final newVisibleTo = From b75606072d221f293d21996055dd701c7ff414f0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 06:46:26 +0100 Subject: [PATCH 02/66] style(content_management): reduce spacing and margin sizes - Change horizontal margin from medium to small in headlines_page.dart - Change column spacing and horizontal margin from medium to small in sources_page.dart and topics_page.dart - Remove source type column in sources_page.dart when on mobile view - Adjust spacing in various data table configurations --- .../view/headlines_page.dart | 2 +- lib/content_management/view/sources_page.dart | 26 ++++++++++--------- lib/content_management/view/topics_page.dart | 4 +-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index d7838a39..c9b90eef 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -176,7 +176,7 @@ class _HeadlinesPageState extends State { headingRowHeight: 56, dataRowHeight: 56, columnSpacing: AppSpacing.sm, - horizontalMargin: AppSpacing.md, + horizontalMargin: AppSpacing.sm, ); }, ), diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index b0c4b8d0..de74b0f5 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -126,10 +126,11 @@ class _SourcesPageState extends State { label: Text(l10n.sourceName), size: ColumnSize.L, ), - DataColumn2( - label: Text(l10n.sourceType), - size: ColumnSize.S, - ), + if (!isMobile) + DataColumn2( + label: Text(l10n.sourceType), + size: ColumnSize.S, + ), DataColumn2( label: Text(l10n.lastUpdated), size: ColumnSize.S, @@ -173,8 +174,8 @@ class _SourcesPageState extends State { fit: FlexFit.tight, headingRowHeight: 56, dataRowHeight: 56, - columnSpacing: AppSpacing.md, - horizontalMargin: AppSpacing.md, + columnSpacing: AppSpacing.sm, + horizontalMargin: AppSpacing.sm, ); }, ), @@ -225,13 +226,14 @@ class _SourcesDataSource extends DataTableSource { overflow: TextOverflow.ellipsis, ), ), - DataCell( - Text( - source.sourceType.localizedName(l10n), - maxLines: 2, - overflow: TextOverflow.ellipsis, + if (!isMobile) + DataCell( + Text( + source.sourceType.localizedName(l10n), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), ), - ), DataCell( Text( DateFormat('dd-MM-yyyy').format(source.updatedAt.toLocal()), diff --git a/lib/content_management/view/topics_page.dart b/lib/content_management/view/topics_page.dart index 049b9f8c..e7152456 100644 --- a/lib/content_management/view/topics_page.dart +++ b/lib/content_management/view/topics_page.dart @@ -165,8 +165,8 @@ class _TopicPageState extends State { fit: FlexFit.tight, headingRowHeight: 56, dataRowHeight: 56, - columnSpacing: AppSpacing.md, - horizontalMargin: AppSpacing.md, + columnSpacing: AppSpacing.sm, + horizontalMargin: AppSpacing.sm, ); }, ), From 8974a29651b4823b232b71e80e558a1232c91ac8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:09:48 +0100 Subject: [PATCH 03/66] feat(l10n): add Arabic translations for admob_flutter plugin - Added Arabic translations for new strings related to AdMob filtering and loading states - Translated terms for different ad types (native, banner, interstitial, video) - Included translations for search functionality and column headers --- lib/l10n/app_localizations.dart | 78 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 39 +++++++++++++++ lib/l10n/app_localizations_en.dart | 39 +++++++++++++++ lib/l10n/arb/app_ar.arb | 56 ++++++++++++++++++++- lib/l10n/arb/app_en.arb | 54 ++++++++++++++++++++- 5 files changed, 263 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 19ecc205..4758ce46 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2725,6 +2725,84 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Close'** String get closeButtonText; + + /// Title for the filter dialog when filtering local ads. + /// + /// In en, this message translates to: + /// **'Filter Local Ads'** + String get filterLocalAds; + + /// Hint text for the ad title or URL search field. + /// + /// In en, this message translates to: + /// **'Search by ad title or URL...'** + String get searchByAdTitleOrUrl; + + /// Label for the ad type filter. + /// + /// In en, this message translates to: + /// **'Ad Type'** + String get adType; + + /// Headline for loading state of native ads. + /// + /// In en, this message translates to: + /// **'Loading Native Ads'** + String get loadingNativeAds; + + /// Message when no native ads are found. + /// + /// In en, this message translates to: + /// **'No native ads found.'** + String get noNativeAdsFound; + + /// Headline for loading state of banner ads. + /// + /// In en, this message translates to: + /// **'Loading Banner Ads'** + String get loadingBannerAds; + + /// Message when no banner ads are found. + /// + /// In en, this message translates to: + /// **'No banner ads found.'** + String get noBannerAdsFound; + + /// Column header for ad image URL. + /// + /// In en, this message translates to: + /// **'Image URL'** + String get adImageUrl; + + /// Headline for loading state of interstitial ads. + /// + /// In en, this message translates to: + /// **'Loading Interstitial Ads'** + String get loadingInterstitialAds; + + /// Message when no interstitial ads are found. + /// + /// In en, this message translates to: + /// **'No interstitial ads found.'** + String get noInterstitialAdsFound; + + /// Headline for loading state of video ads. + /// + /// In en, this message translates to: + /// **'Loading Video Ads'** + String get loadingVideoAds; + + /// Message when no video ads are found. + /// + /// In en, this message translates to: + /// **'No video ads found.'** + String get noVideoAdsFound; + + /// Column header for ad video URL. + /// + /// In en, this message translates to: + /// **'Video URL'** + String get adVideoUrl; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 940dc8ff..13d26bd5 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1454,4 +1454,43 @@ class AppLocalizationsAr extends AppLocalizations { @override String get closeButtonText => 'إغلاق'; + + @override + String get filterLocalAds => 'تصفية الإعلانات المحلية'; + + @override + String get searchByAdTitleOrUrl => 'البحث بعنوان الإعلان أو الرابط...'; + + @override + String get adType => 'نوع الإعلان'; + + @override + String get loadingNativeAds => 'جاري تحميل الإعلانات الأصلية'; + + @override + String get noNativeAdsFound => 'لم يتم العثور على إعلانات أصلية.'; + + @override + String get loadingBannerAds => 'جاري تحميل إعلانات البانر'; + + @override + String get noBannerAdsFound => 'لم يتم العثور على إعلانات بانر.'; + + @override + String get adImageUrl => 'رابط الصورة'; + + @override + String get loadingInterstitialAds => 'جاري تحميل الإعلانات البينية'; + + @override + String get noInterstitialAdsFound => 'لم يتم العثور على إعلانات بينية.'; + + @override + String get loadingVideoAds => 'جاري تحميل إعلانات الفيديو'; + + @override + String get noVideoAdsFound => 'لم يتم العثور على إعلانات فيديو.'; + + @override + String get adVideoUrl => 'رابط الفيديو'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b0b89aa6..319ac03e 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1459,4 +1459,43 @@ class AppLocalizationsEn extends AppLocalizations { @override String get closeButtonText => 'Close'; + + @override + String get filterLocalAds => 'Filter Local Ads'; + + @override + String get searchByAdTitleOrUrl => 'Search by ad title or URL...'; + + @override + String get adType => 'Ad Type'; + + @override + String get loadingNativeAds => 'Loading Native Ads'; + + @override + String get noNativeAdsFound => 'No native ads found.'; + + @override + String get loadingBannerAds => 'Loading Banner Ads'; + + @override + String get noBannerAdsFound => 'No banner ads found.'; + + @override + String get adImageUrl => 'Image URL'; + + @override + String get loadingInterstitialAds => 'Loading Interstitial Ads'; + + @override + String get noInterstitialAdsFound => 'No interstitial ads found.'; + + @override + String get loadingVideoAds => 'Loading Video Ads'; + + @override + String get noVideoAdsFound => 'No video ads found.'; + + @override + String get adVideoUrl => 'Video URL'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 2a250568..c1d801f0 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1840,8 +1840,60 @@ "@aboutIconTooltip": { "description": "Tooltip for the information icon that shows page description." }, - "closeButtonText": "إغلاق", +"closeButtonText": "إغلاق", "@closeButtonText": { "description": "Text for the close button in a dialog." + }, + "filterLocalAds": "تصفية الإعلانات المحلية", + "@filterLocalAds": { + "description": "عنوان مربع حوار التصفية عند تصفية الإعلانات المحلية." + }, + "searchByAdTitleOrUrl": "البحث بعنوان الإعلان أو الرابط...", + "@searchByAdTitleOrUrl": { + "description": "نص تلميح حقل البحث عن عنوان الإعلان أو الرابط." + }, + "adType": "نوع الإعلان", + "@adType": { + "description": "تسمية لفلتر نوع الإعلان." + }, + "loadingNativeAds": "جاري تحميل الإعلانات الأصلية", + "@loadingNativeAds": { + "description": "عنوان حالة تحميل الإعلانات الأصلية." + }, + "noNativeAdsFound": "لم يتم العثور على إعلانات أصلية.", + "@noNativeAdsFound": { + "description": "رسالة عند عدم العثور على إعلانات أصلية." + }, + "loadingBannerAds": "جاري تحميل إعلانات البانر", + "@loadingBannerAds": { + "description": "عنوان حالة تحميل إعلانات البانر." + }, + "noBannerAdsFound": "لم يتم العثور على إعلانات بانر.", + "@noBannerAdsFound": { + "description": "رسالة عند عدم العثور على إعلانات بانر." + }, + "adImageUrl": "رابط الصورة", + "@adImageUrl": { + "description": "رأس العمود لرابط صورة الإعلان." + }, + "loadingInterstitialAds": "جاري تحميل الإعلانات البينية", + "@loadingInterstitialAds": { + "description": "عنوان حالة تحميل الإعلانات البينية." + }, + "noInterstitialAdsFound": "لم يتم العثور على إعلانات بينية.", + "@noInterstitialAdsFound": { + "description": "رسالة عند عدم العثور على إعلانات بينية." + }, + "loadingVideoAds": "جاري تحميل إعلانات الفيديو", + "@loadingVideoAds": { + "description": "عنوان حالة تحميل إعلانات الفيديو." + }, + "noVideoAdsFound": "لم يتم العثور على إعلانات فيديو.", + "@noVideoAdsFound": { + "description": "رسالة عند عدم العثور على إعلانات فيديو." + }, + "adVideoUrl": "رابط الفيديو", + "@adVideoUrl": { + "description": "رأس العمود لرابط فيديو الإعلان." } -} \ No newline at end of file +} diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index db98ebb4..72da7bf1 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1839,5 +1839,57 @@ "closeButtonText": "Close", "@closeButtonText": { "description": "Text for the close button in a dialog." + }, + "filterLocalAds": "Filter Local Ads", + "@filterLocalAds": { + "description": "Title for the filter dialog when filtering local ads." + }, + "searchByAdTitleOrUrl": "Search by ad title or URL...", + "@searchByAdTitleOrUrl": { + "description": "Hint text for the ad title or URL search field." + }, + "adType": "Ad Type", + "@adType": { + "description": "Label for the ad type filter." + }, + "loadingNativeAds": "Loading Native Ads", + "@loadingNativeAds": { + "description": "Headline for loading state of native ads." + }, + "noNativeAdsFound": "No native ads found.", + "@noNativeAdsFound": { + "description": "Message when no native ads are found." + }, + "loadingBannerAds": "Loading Banner Ads", + "@loadingBannerAds": { + "description": "Headline for loading state of banner ads." + }, + "noBannerAdsFound": "No banner ads found.", + "@noBannerAdsFound": { + "description": "Message when no banner ads are found." + }, + "adImageUrl": "Image URL", + "@adImageUrl": { + "description": "Column header for ad image URL." + }, + "loadingInterstitialAds": "Loading Interstitial Ads", + "@loadingInterstitialAds": { + "description": "Headline for loading state of interstitial ads." + }, + "noInterstitialAdsFound": "No interstitial ads found.", + "@noInterstitialAdsFound": { + "description": "Message when no interstitial ads are found." + }, + "loadingVideoAds": "Loading Video Ads", + "@loadingVideoAds": { + "description": "Headline for loading state of video ads." + }, + "noVideoAdsFound": "No video ads found.", + "@noVideoAdsFound": { + "description": "Message when no video ads are found." + }, + "adVideoUrl": "Video URL", + "@adVideoUrl": { + "description": "Column header for ad video URL." } -} \ No newline at end of file +} From 94687493f2a42270b7e8e2a4badbda62bd8f1014 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:54:06 +0100 Subject: [PATCH 04/66] refactor(local_ads_management): update events and add deletion handling - Remove AdType from LoadLocalAdsRequested, ArchiveLocalAdRequested, and RestoreLocalAdRequested events - Add filter option to LoadLocalAdsRequested event - Implement DeletionEventReceived event for handling deletion events from PendingDeletionsService - Update props list for affected events - Remove UndoDeleteLocalAdRequested event --- .../bloc/local_ads_management_event.dart | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/local_ads_management/bloc/local_ads_management_event.dart b/lib/local_ads_management/bloc/local_ads_management_event.dart index abaec98d..b04aab6e 100644 --- a/lib/local_ads_management/bloc/local_ads_management_event.dart +++ b/lib/local_ads_management/bloc/local_ads_management_event.dart @@ -27,15 +27,12 @@ final class LocalAdsManagementTabChanged extends LocalAdsManagementEvent { final class LoadLocalAdsRequested extends LocalAdsManagementEvent { /// {@macro load_local_ads_requested} const LoadLocalAdsRequested({ - required this.adType, this.startAfterId, this.limit, this.forceRefresh = false, + this.filter, }); - /// The type of ad to load. - final AdType adType; - /// Optional ID to start pagination after. final String? startAfterId; @@ -45,8 +42,11 @@ final class LoadLocalAdsRequested extends LocalAdsManagementEvent { /// If true, forces a refresh of the data, bypassing the cache. final bool forceRefresh; + /// Optional filter to apply to the local ads query. + final Map? filter; + @override - List get props => [adType, startAfterId, limit, forceRefresh]; + List get props => [startAfterId, limit, forceRefresh, filter]; } /// {@template archive_local_ad_requested} @@ -54,16 +54,13 @@ final class LoadLocalAdsRequested extends LocalAdsManagementEvent { /// {@endtemplate} final class ArchiveLocalAdRequested extends LocalAdsManagementEvent { /// {@macro archive_local_ad_requested} - const ArchiveLocalAdRequested(this.id, this.adType); + const ArchiveLocalAdRequested(this.id); /// The ID of the local ad to archive. final String id; - /// The type of the local ad to archive. - final AdType adType; - @override - List get props => [id, adType]; + List get props => [id]; } /// {@template restore_local_ad_requested} @@ -71,16 +68,13 @@ final class ArchiveLocalAdRequested extends LocalAdsManagementEvent { /// {@endtemplate} final class RestoreLocalAdRequested extends LocalAdsManagementEvent { /// {@macro restore_local_ad_requested} - const RestoreLocalAdRequested(this.id, this.adType); + const RestoreLocalAdRequested(this.id); /// The ID of the local ad to restore. final String id; - /// The type of the local ad to restore. - final AdType adType; - @override - List get props => [id, adType]; + List get props => [id]; } /// {@template delete_local_ad_forever_requested} @@ -102,17 +96,21 @@ final class DeleteLocalAdForeverRequested extends LocalAdsManagementEvent { /// {@endtemplate} final class UndoDeleteLocalAdRequested extends LocalAdsManagementEvent { /// {@macro undo_delete_local_ad_requested} - const UndoDeleteLocalAdRequested(); -} + const UndoDeleteLocalAdRequested(this.id); -/// Internal event to confirm permanent deletion after a delay. -final class _ConfirmDeleteLocalAdRequested extends LocalAdsManagementEvent { - /// {@macro _confirm_delete_local_ad_requested} - const _ConfirmDeleteLocalAdRequested(this.id); - - /// The ID of the local ad to confirm deletion for. + /// The ID of the local ad to undo deletion for. final String id; @override List get props => [id]; } + +/// Event received when a deletion event occurs in the PendingDeletionsService. +final class DeletionEventReceived extends LocalAdsManagementEvent { + const DeletionEventReceived(this.event); + + final DeletionEvent event; + + @override + List get props => [event]; +} From 3bfbd09c98539d256ef83a2ed00e15edb2ad1125 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:54:16 +0100 Subject: [PATCH 05/66] refactor(local_ads_management): replace lastDeletedLocalAd with snackbarMessage - Remove lastDeletedLocalAd property from LocalAdsManagementState - Add snackbarMessage property for displaying transient messages - Update copyWith method to accommodate the new snackbarMessage property - Adjust props list to include snackbarMessage instead of lastDeletedLocalAd --- .../bloc/local_ads_management_state.dart | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/local_ads_management/bloc/local_ads_management_state.dart b/lib/local_ads_management/bloc/local_ads_management_state.dart index 943a0a6b..6ffdcc64 100644 --- a/lib/local_ads_management/bloc/local_ads_management_state.dart +++ b/lib/local_ads_management/bloc/local_ads_management_state.dart @@ -52,7 +52,7 @@ class LocalAdsManagementState extends Equatable { this.videoAdsCursor, this.videoAdsHasMore = false, this.exception, - this.lastDeletedLocalAd, + this.snackbarMessage, }); /// The currently active tab in the local ads management section. @@ -106,11 +106,12 @@ class LocalAdsManagementState extends Equatable { /// Indicates if there are more video ads to load. final bool videoAdsHasMore; - /// The error describing an operation failure, if any. + /// The exception encountered during a failed operation, if any. final HttpException? exception; - /// The last deleted local ad, used for undo functionality. - final LocalAd? lastDeletedLocalAd; + /// The message to display in the snackbar for pending deletions or other + /// transient messages. + final String? snackbarMessage; /// Creates a copy of this [LocalAdsManagementState] with updated values. LocalAdsManagementState copyWith({ @@ -132,8 +133,7 @@ class LocalAdsManagementState extends Equatable { String? videoAdsCursor, bool? videoAdsHasMore, HttpException? exception, - LocalAd? lastDeletedLocalAd, - bool clearLastDeletedLocalAd = false, + String? snackbarMessage, }) { return LocalAdsManagementState( activeTab: activeTab ?? this.activeTab, @@ -156,10 +156,8 @@ class LocalAdsManagementState extends Equatable { videoAds: videoAds ?? this.videoAds, videoAdsCursor: videoAdsCursor ?? this.videoAdsCursor, videoAdsHasMore: videoAdsHasMore ?? this.videoAdsHasMore, - exception: exception ?? this.exception, - lastDeletedLocalAd: clearLastDeletedLocalAd - ? null - : lastDeletedLocalAd ?? this.lastDeletedLocalAd, + exception: exception, + snackbarMessage: snackbarMessage, ); } @@ -183,6 +181,6 @@ class LocalAdsManagementState extends Equatable { videoAdsCursor, videoAdsHasMore, exception, - lastDeletedLocalAd, + snackbarMessage, ]; } From 2756a0be74d1550baddb8a6ec49304c3c9746163 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:55:36 +0100 Subject: [PATCH 06/66] refactor(local_ads_management): improve ad deletion and filtering - Implement undoable deletions using PendingDeletionsService - Add real-time filtering functionality - Enhance UI/UX with snackbar messages for actions - Optimize code structure and improve readability --- .../bloc/local_ads_management_bloc.dart | 405 ++++++++---------- 1 file changed, 180 insertions(+), 225 deletions(-) diff --git a/lib/local_ads_management/bloc/local_ads_management_bloc.dart b/lib/local_ads_management/bloc/local_ads_management_bloc.dart index 5db898af..c7a7e815 100644 --- a/lib/local_ads_management/bloc/local_ads_management_bloc.dart +++ b/lib/local_ads_management/bloc/local_ads_management_bloc.dart @@ -1,9 +1,14 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:collection/collection.dart'; // Import for firstWhereOrNull import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.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/extensions/string_truncate.dart'; // Import for truncate +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; import 'package:ui_kit/ui_kit.dart'; part 'local_ads_management_event.dart'; @@ -13,7 +18,11 @@ class LocalAdsManagementBloc extends Bloc { LocalAdsManagementBloc({ required DataRepository localAdsRepository, + required FilterLocalAdsBloc filterLocalAdsBloc, + required PendingDeletionsService pendingDeletionsService, }) : _localAdsRepository = localAdsRepository, + _filterLocalAdsBloc = filterLocalAdsBloc, + _pendingDeletionsService = pendingDeletionsService, super(const LocalAdsManagementState()) { on(_onLocalAdsManagementTabChanged); on(_onLoadLocalAdsRequested); @@ -21,53 +30,78 @@ class LocalAdsManagementBloc on(_onRestoreLocalAdRequested); on(_onDeleteLocalAdForeverRequested); on(_onUndoDeleteLocalAdRequested); - on<_ConfirmDeleteLocalAdRequested>(_onConfirmDeleteLocalAdRequested); + on(_onDeletionEventReceived); _localAdUpdateSubscription = _localAdsRepository.entityUpdated .where((type) => type == LocalAd) .listen((_) { add( - const LoadLocalAdsRequested( - adType: AdType.native, - limit: kDefaultRowsPerPage, - forceRefresh: true, - ), - ); - add( - const LoadLocalAdsRequested( - adType: AdType.banner, - limit: kDefaultRowsPerPage, - forceRefresh: true, - ), - ); - add( - const LoadLocalAdsRequested( - adType: AdType.interstitial, - limit: kDefaultRowsPerPage, - forceRefresh: true, - ), - ); - add( - const LoadLocalAdsRequested( - adType: AdType.video, + LoadLocalAdsRequested( limit: kDefaultRowsPerPage, forceRefresh: true, + filter: buildLocalAdsFilterMap(_filterLocalAdsBloc.state), ), ); }); + + _filterLocalAdsSubscription = _filterLocalAdsBloc.stream.listen((_) { + add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: buildLocalAdsFilterMap(_filterLocalAdsBloc.state), + ), + ); + }); + + _deletionEventsSubscription = _pendingDeletionsService.deletionEvents + .listen( + (event) => add(DeletionEventReceived(event)), + ); } final DataRepository _localAdsRepository; + final FilterLocalAdsBloc _filterLocalAdsBloc; + final PendingDeletionsService _pendingDeletionsService; + late final StreamSubscription _localAdUpdateSubscription; - Timer? _deleteTimer; + late final StreamSubscription + _filterLocalAdsSubscription; + late final StreamSubscription> + _deletionEventsSubscription; @override Future close() { _localAdUpdateSubscription.cancel(); - _deleteTimer?.cancel(); + _filterLocalAdsSubscription.cancel(); + _deletionEventsSubscription.cancel(); return super.close(); } + /// Builds a filter map for local ads from the given filter state. + Map buildLocalAdsFilterMap(FilterLocalAdsState state) { + final filter = {}; + + if (state.searchQuery.isNotEmpty) { + filter[r'$or'] = [ + { + 'title': {r'$regex': state.searchQuery, r'$options': 'i'}, + }, + { + 'imageUrl': {r'$regex': state.searchQuery, r'$options': 'i'}, + }, + { + 'videoUrl': {r'$regex': state.searchQuery, r'$options': 'i'}, + }, + ]; + } + + filter['status'] = state.selectedStatus.name; + filter['adType'] = state.selectedAdType.name; + + return filter; + } + void _onLocalAdsManagementTabChanged( LocalAdsManagementTabChanged event, Emitter emit, @@ -80,12 +114,15 @@ class LocalAdsManagementBloc Emitter emit, ) async { // Determine current state and emit loading status - switch (event.adType) { + final currentFilterAdType = _filterLocalAdsBloc.state.selectedAdType; + + switch (currentFilterAdType) { case AdType.native: if (state.nativeAdsStatus == LocalAdsManagementStatus.success && state.nativeAds.isNotEmpty && event.startAfterId == null && - !event.forceRefresh) { + !event.forceRefresh && + event.filter == null) { return; } emit(state.copyWith(nativeAdsStatus: LocalAdsManagementStatus.loading)); @@ -93,7 +130,8 @@ class LocalAdsManagementBloc if (state.bannerAdsStatus == LocalAdsManagementStatus.success && state.bannerAds.isNotEmpty && event.startAfterId == null && - !event.forceRefresh) { + !event.forceRefresh && + event.filter == null) { return; } emit(state.copyWith(bannerAdsStatus: LocalAdsManagementStatus.loading)); @@ -101,7 +139,8 @@ class LocalAdsManagementBloc if (state.interstitialAdsStatus == LocalAdsManagementStatus.success && state.interstitialAds.isNotEmpty && event.startAfterId == null && - !event.forceRefresh) { + !event.forceRefresh && + event.filter == null) { return; } emit( @@ -113,7 +152,8 @@ class LocalAdsManagementBloc if (state.videoAdsStatus == LocalAdsManagementStatus.success && state.videoAds.isNotEmpty && event.startAfterId == null && - !event.forceRefresh) { + !event.forceRefresh && + event.filter == null) { return; } emit(state.copyWith(videoAdsStatus: LocalAdsManagementStatus.loading)); @@ -122,10 +162,8 @@ class LocalAdsManagementBloc try { final isPaginating = event.startAfterId != null; final paginatedAds = await _localAdsRepository.readAll( - filter: { - 'adType': event.adType.name, - 'status': ContentStatus.active.name, - }, + filter: + event.filter ?? buildLocalAdsFilterMap(_filterLocalAdsBloc.state), sort: [const SortOption('updatedAt', SortOrder.desc)], pagination: PaginationOptions( cursor: event.startAfterId, @@ -133,7 +171,7 @@ class LocalAdsManagementBloc ), ); - switch (event.adType) { + switch (currentFilterAdType) { case AdType.native: final previousAds = isPaginating ? state.nativeAds @@ -147,6 +185,7 @@ class LocalAdsManagementBloc ], nativeAdsCursor: paginatedAds.cursor, nativeAdsHasMore: paginatedAds.hasMore, + exception: null, // Clear any previous exception ), ); case AdType.banner: @@ -162,6 +201,7 @@ class LocalAdsManagementBloc ], bannerAdsCursor: paginatedAds.cursor, bannerAdsHasMore: paginatedAds.hasMore, + exception: null, // Clear any previous exception ), ); case AdType.interstitial: @@ -177,6 +217,7 @@ class LocalAdsManagementBloc ], interstitialAdsCursor: paginatedAds.cursor, interstitialAdsHasMore: paginatedAds.hasMore, + exception: null, // Clear any previous exception ), ); case AdType.video: @@ -190,11 +231,12 @@ class LocalAdsManagementBloc ], videoAdsCursor: paginatedAds.cursor, videoAdsHasMore: paginatedAds.hasMore, + exception: null, // Clear any previous exception ), ); } } on HttpException catch (e) { - switch (event.adType) { + switch (currentFilterAdType) { case AdType.native: emit( state.copyWith( @@ -225,7 +267,7 @@ class LocalAdsManagementBloc ); } } catch (e) { - switch (event.adType) { + switch (currentFilterAdType) { case AdType.native: emit( state.copyWith( @@ -262,45 +304,9 @@ class LocalAdsManagementBloc ArchiveLocalAdRequested event, Emitter emit, ) async { - LocalAd? adToArchive; - final originalNativeAds = List.from(state.nativeAds); - final originalBannerAds = List.from(state.bannerAds); - final originalInterstitialAds = List.from( - state.interstitialAds, - ); - final originalVideoAds = List.from(state.videoAds); - - switch (event.adType) { - case AdType.native: - final index = originalNativeAds.indexWhere((ad) => ad.id == event.id); - if (index == -1) return; - adToArchive = originalNativeAds[index]; - originalNativeAds.removeAt(index); - emit(state.copyWith(nativeAds: originalNativeAds)); - case AdType.banner: - final index = originalBannerAds.indexWhere((ad) => ad.id == event.id); - if (index == -1) return; - adToArchive = originalBannerAds[index]; - originalBannerAds.removeAt(index); - emit(state.copyWith(bannerAds: originalBannerAds)); - case AdType.interstitial: - final index = originalInterstitialAds.indexWhere( - (ad) => ad.id == event.id, - ); - if (index == -1) return; - adToArchive = originalInterstitialAds[index]; - originalInterstitialAds.removeAt(index); - emit(state.copyWith(interstitialAds: originalInterstitialAds)); - case AdType.video: - final index = originalVideoAds.indexWhere((ad) => ad.id == event.id); - if (index == -1) return; - adToArchive = originalVideoAds[index]; - originalVideoAds.removeAt(index); - emit(state.copyWith(videoAds: originalVideoAds)); - } - try { - final updatedAd = switch (adToArchive) { + final adToUpdate = await _localAdsRepository.read(id: event.id); + final updatedAd = switch (adToUpdate) { final LocalNativeAd ad => ad.copyWith(status: ContentStatus.archived), final LocalBannerAd ad => ad.copyWith(status: ContentStatus.archived), final LocalInterstitialAd ad => ad.copyWith( @@ -308,37 +314,22 @@ class LocalAdsManagementBloc ), final LocalVideoAd ad => ad.copyWith(status: ContentStatus.archived), _ => throw StateError( - 'Unknown LocalAd type: ${adToArchive.runtimeType}', + 'Unknown LocalAd type: ${adToUpdate.runtimeType}', ), }; await _localAdsRepository.update( id: event.id, item: updatedAd, ); + emit( + state.copyWith( + snackbarMessage: 'Ad archived successfully.', + exception: null, + ), + ); // Clear exception on success } on HttpException catch (e) { - // Revert UI on failure - switch (event.adType) { - case AdType.native: - emit(state.copyWith(nativeAds: originalNativeAds)); - case AdType.banner: - emit(state.copyWith(bannerAds: originalBannerAds)); - case AdType.interstitial: - emit(state.copyWith(interstitialAds: originalInterstitialAds)); - case AdType.video: - emit(state.copyWith(videoAds: originalVideoAds)); - } emit(state.copyWith(exception: e)); } catch (e) { - switch (event.adType) { - case AdType.native: - emit(state.copyWith(nativeAds: originalNativeAds)); - case AdType.banner: - emit(state.copyWith(bannerAds: originalBannerAds)); - case AdType.interstitial: - emit(state.copyWith(interstitialAds: originalInterstitialAds)); - case AdType.video: - emit(state.copyWith(videoAds: originalVideoAds)); - } emit( state.copyWith( exception: UnknownException('An unexpected error occurred: $e'), @@ -351,12 +342,7 @@ class LocalAdsManagementBloc RestoreLocalAdRequested event, Emitter emit, ) async { - // This event is primarily for the archived ads page, but the bloc - // needs to know about it to trigger a refresh of the active lists. - // The actual UI update for the archived list will be handled by the - // ArchivedLocalAdsBloc. try { - // Fetch the ad to restore (it's currently archived) final adToRestore = await _localAdsRepository.read(id: event.id); final updatedAd = switch (adToRestore) { final LocalNativeAd ad => ad.copyWith(status: ContentStatus.active), @@ -373,7 +359,12 @@ class LocalAdsManagementBloc id: event.id, item: updatedAd, ); - // The entityUpdated stream will trigger a reload of active ads. + emit( + state.copyWith( + snackbarMessage: 'Ad restored successfully.', + exception: null, + ), + ); // Clear exception on success } on HttpException catch (e) { emit(state.copyWith(exception: e)); } catch (e) { @@ -389,30 +380,23 @@ class LocalAdsManagementBloc DeleteLocalAdForeverRequested event, Emitter emit, ) async { - _deleteTimer?.cancel(); - LocalAd? adToDelete; - final currentNativeAds = List.from(state.nativeAds); - final currentBannerAds = List.from(state.bannerAds); - final currentInterstitialAds = List.from( - state.interstitialAds, - ); - final currentVideoAds = List.from(state.videoAds); - - // Find and remove the ad from the current active list - var index = -1; - if (state.activeTab == LocalAdsManagementTab.native) { - index = currentNativeAds.indexWhere((ad) => ad.id == event.id); - if (index != -1) adToDelete = currentNativeAds[index]; - } else if (state.activeTab == LocalAdsManagementTab.banner) { - index = currentBannerAds.indexWhere((ad) => ad.id == event.id); - if (index != -1) adToDelete = currentBannerAds[index]; - } else if (state.activeTab == LocalAdsManagementTab.interstitial) { - index = currentInterstitialAds.indexWhere((ad) => ad.id == event.id); - if (index != -1) adToDelete = currentInterstitialAds[index]; - } else if (state.activeTab == LocalAdsManagementTab.video) { - index = currentVideoAds.indexWhere((ad) => ad.id == event.id); - if (index != -1) adToDelete = currentVideoAds[index]; + // Find the ad to delete from the current active list based on the active tab + switch (state.activeTab) { + case LocalAdsManagementTab.native: + adToDelete = state.nativeAds.firstWhereOrNull( + (ad) => ad.id == event.id, + ); + case LocalAdsManagementTab.banner: + adToDelete = state.bannerAds.firstWhereOrNull( + (ad) => ad.id == event.id, + ); + case LocalAdsManagementTab.interstitial: + adToDelete = state.interstitialAds.firstWhereOrNull( + (ad) => ad.id == event.id, + ); + case LocalAdsManagementTab.video: + adToDelete = state.videoAds.firstWhereOrNull((ad) => ad.id == event.id); } if (adToDelete == null) return; @@ -420,140 +404,111 @@ class LocalAdsManagementBloc // Optimistically remove from UI switch (adToDelete.adType) { case 'native': - currentNativeAds.removeWhere((ad) => ad.id == event.id); - emit(state.copyWith(nativeAds: currentNativeAds)); + final updatedAds = List.from(state.nativeAds) + ..removeWhere((ad) => ad.id == event.id); + emit(state.copyWith(nativeAds: updatedAds)); case 'banner': - currentBannerAds.removeWhere((ad) => ad.id == event.id); - emit(state.copyWith(bannerAds: currentBannerAds)); + final updatedAds = List.from(state.bannerAds) + ..removeWhere((ad) => ad.id == event.id); + emit(state.copyWith(bannerAds: updatedAds)); case 'interstitial': - currentInterstitialAds.removeWhere((ad) => ad.id == event.id); - emit(state.copyWith(interstitialAds: currentInterstitialAds)); + final updatedAds = List.from(state.interstitialAds) + ..removeWhere((ad) => ad.id == event.id); + emit(state.copyWith(interstitialAds: updatedAds)); case 'video': - currentVideoAds.removeWhere((ad) => ad.id == event.id); - emit(state.copyWith(videoAds: currentVideoAds)); + final updatedAds = List.from(state.videoAds) + ..removeWhere((ad) => ad.id == event.id); + emit(state.copyWith(videoAds: updatedAds)); } - emit(state.copyWith(lastDeletedLocalAd: adToDelete)); + emit( + state.copyWith( + snackbarMessage: 'Ad "${adToDelete.id.truncate(30)}" deleted.', + exception: null, + ), + ); // Clear exception on success - _deleteTimer = Timer( - const Duration(seconds: 5), - () => add(_ConfirmDeleteLocalAdRequested(event.id)), + _pendingDeletionsService.requestDeletion( + item: adToDelete, + repository: _localAdsRepository, + undoDuration: AppConstants.kSnackbarDuration, ); } - Future _onConfirmDeleteLocalAdRequested( - _ConfirmDeleteLocalAdRequested event, - Emitter emit, - ) async { - try { - await _localAdsRepository.delete(id: event.id); - emit(state.copyWith(clearLastDeletedLocalAd: true)); - } on HttpException catch (e) { - // If deletion fails, restore the ad to the list - if (state.lastDeletedLocalAd != null) { - final restoredAd = state.lastDeletedLocalAd!; - switch (restoredAd.adType) { - case 'native': - final updatedAds = List.from(state.nativeAds) - ..add(restoredAd as LocalNativeAd); - emit(state.copyWith(nativeAds: updatedAds)); - case 'banner': - final updatedAds = List.from(state.bannerAds) - ..add(restoredAd as LocalBannerAd); - emit(state.copyWith(bannerAds: updatedAds)); - case 'interstitial': - final updatedAds = List.from( - state.interstitialAds, - )..add(restoredAd as LocalInterstitialAd); - emit(state.copyWith(interstitialAds: updatedAds)); - case 'video': - final updatedAds = List.from(state.videoAds) - ..add(restoredAd as LocalVideoAd); - emit(state.copyWith(videoAds: updatedAds)); - } - } - emit(state.copyWith(exception: e, clearLastDeletedLocalAd: true)); - } catch (e) { - if (state.lastDeletedLocalAd != null) { - final restoredAd = state.lastDeletedLocalAd!; - switch (restoredAd.adType) { - case 'native': - final updatedAds = List.from(state.nativeAds) - ..add(restoredAd as LocalNativeAd); - emit(state.copyWith(nativeAds: updatedAds)); - case 'banner': - final updatedAds = List.from(state.bannerAds) - ..add(restoredAd as LocalBannerAd); - emit(state.copyWith(bannerAds: updatedAds)); - case 'interstitial': - final updatedAds = List.from( - state.interstitialAds, - )..add(restoredAd as LocalInterstitialAd); - emit(state.copyWith(interstitialAds: updatedAds)); - case 'video': - final updatedAds = List.from(state.videoAds) - ..add(restoredAd as LocalVideoAd); - emit(state.copyWith(videoAds: updatedAds)); - } - } - emit( - state.copyWith( - exception: UnknownException('An unexpected error occurred: $e'), - clearLastDeletedLocalAd: true, - ), - ); - } - } - void _onUndoDeleteLocalAdRequested( UndoDeleteLocalAdRequested event, Emitter emit, ) { - _deleteTimer?.cancel(); - if (state.lastDeletedLocalAd != null) { - final restoredAd = state.lastDeletedLocalAd!; - switch (restoredAd.adType) { - case 'native': + _pendingDeletionsService.undoDeletion(event.id); + } + + /// Handles deletion events from the [PendingDeletionsService]. + /// + /// This method is responsible for updating the BLoC state based on whether + /// a deletion was confirmed or undone. + Future _onDeletionEventReceived( + DeletionEventReceived event, + Emitter emit, + ) async { + switch (event.event.status) { + case DeletionStatus.confirmed: + // If deletion is confirmed, clear pending status. + // The item was already optimistically removed from the list. + emit( + state.copyWith( + snackbarMessage: null, + exception: null, // Clear any previous exception + ), + ); + case DeletionStatus.undone: + // If deletion is undone, re-add the item to the appropriate list. + final item = event.event.item; + if (item is LocalNativeAd) { final updatedAds = List.from(state.nativeAds) - ..insert( - 0, - restoredAd as LocalNativeAd, - ); + ..add(item) + ..sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); emit( state.copyWith( nativeAds: updatedAds, - clearLastDeletedLocalAd: true, + snackbarMessage: null, + exception: null, // Clear any previous exception ), ); - case 'banner': + } else if (item is LocalBannerAd) { final updatedAds = List.from(state.bannerAds) - ..insert(0, restoredAd as LocalBannerAd); + ..add(item) + ..sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); emit( state.copyWith( bannerAds: updatedAds, - clearLastDeletedLocalAd: true, + snackbarMessage: null, + exception: null, // Clear any previous exception ), ); - case 'interstitial': - final updatedAds = List.from( - state.interstitialAds, - )..insert(0, restoredAd as LocalInterstitialAd); + } else if (item is LocalInterstitialAd) { + final updatedAds = + List.from(state.interstitialAds) + ..add(item) + ..sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); emit( state.copyWith( interstitialAds: updatedAds, - clearLastDeletedLocalAd: true, + snackbarMessage: null, + exception: null, // Clear any previous exception ), ); - case 'video': + } else if (item is LocalVideoAd) { final updatedAds = List.from(state.videoAds) - ..insert(0, restoredAd as LocalVideoAd); + ..add(item) + ..sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); emit( state.copyWith( videoAds: updatedAds, - clearLastDeletedLocalAd: true, + snackbarMessage: null, + exception: null, // Clear any previous exception ), ); - } + } } } } From d318aaf48fad5becc627f1cd75ecd19170de34ed Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:55:45 +0100 Subject: [PATCH 07/66] feat(local_ads_management): implement filter local ads bloc - Add FilterLocalAdsBloc to manage the state of local ads filter UI - Implement event handlers for search query, status, and ad type changes - Add logic to apply and reset filters - Include a debounce transformer for search input --- .../filter_local_ads_bloc.dart | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart diff --git a/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart new file mode 100644 index 00000000..0ffbd74d --- /dev/null +++ b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart @@ -0,0 +1,88 @@ +import 'package:bloc/bloc.dart'; +import 'package:core/core.dart'; +import 'package:equatable/equatable.dart'; +import 'package:rxdart/rxdart.dart'; + +part 'filter_local_ads_event.dart'; +part 'filter_local_ads_state.dart'; + +/// A transformer to debounce events, typically used for search input. +EventTransformer debounce(Duration duration) { + return (events, mapper) => events.debounceTime(duration).flatMap(mapper); +} + +/// {@template filter_local_ads_bloc} +/// A BLoC that manages the state of the local ads filter UI. +/// +/// It handles user input for search queries, status selections, and ad type +/// criteria, and builds a filter map to be used by the data-fetching BLoC. +/// Filters are applied only when explicitly requested via [FilterLocalAdsApplied]. +/// {@endtemplate} +class FilterLocalAdsBloc + extends Bloc { + /// {@macro filter_local_ads_bloc} + FilterLocalAdsBloc() : super(const FilterLocalAdsState()) { + on( + _onFilterLocalAdsSearchQueryChanged, + transformer: debounce(const Duration(milliseconds: 300)), + ); + on(_onFilterLocalAdsStatusChanged); + on(_onFilterLocalAdsAdTypeChanged); + on(_onFilterLocalAdsApplied); + on(_onFilterLocalAdsReset); + } + + /// Handles changes to the search query text field. + void _onFilterLocalAdsSearchQueryChanged( + FilterLocalAdsSearchQueryChanged event, + Emitter emit, + ) { + emit(state.copyWith(searchQuery: event.query)); + } + + /// Handles changes to the selected content status. + /// + /// This updates the single selected status for the filter. + void _onFilterLocalAdsStatusChanged( + FilterLocalAdsStatusChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedStatus: event.status)); + } + + /// Handles changes to the selected ad type. + /// + /// This updates the single selected ad type for the filter. + void _onFilterLocalAdsAdTypeChanged( + FilterLocalAdsAdTypeChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedAdType: event.adType)); + } + + /// Handles the application of all current filter settings. + /// + /// This event is dispatched when the user explicitly confirms the filters + /// (e.g., by clicking an "Apply" button). It updates the BLoC's state + /// with the final filter values. + void _onFilterLocalAdsApplied( + FilterLocalAdsApplied event, + Emitter emit, + ) { + emit( + state.copyWith( + searchQuery: event.searchQuery, + selectedStatus: event.selectedStatus, + selectedAdType: event.selectedAdType, + ), + ); + } + + /// Handles the request to reset all filters to their initial state. + void _onFilterLocalAdsReset( + FilterLocalAdsReset event, + Emitter emit, + ) { + emit(const FilterLocalAdsState()); + } +} From 2420824264e1c2fa0a3a1912a877521f57c67c52 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:56:19 +0100 Subject: [PATCH 08/66] feat(local_ads_management): implement filter local ads events - Add FilterLocalAdsEvent base class - Implement individual events for search query change, status change, ad type change, applying filters, and resetting filters - Ensure proper Equatable implementation for each event --- .../filter_local_ads_event.dart | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_event.dart diff --git a/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_event.dart b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_event.dart new file mode 100644 index 00000000..1901873a --- /dev/null +++ b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_event.dart @@ -0,0 +1,63 @@ +part of 'filter_local_ads_bloc.dart'; + +sealed class FilterLocalAdsEvent extends Equatable { + const FilterLocalAdsEvent(); + + @override + List get props => []; +} + +/// Event to notify the BLoC that the search query has changed. +final class FilterLocalAdsSearchQueryChanged extends FilterLocalAdsEvent { + const FilterLocalAdsSearchQueryChanged(this.query); + + final String query; + + @override + List get props => [query]; +} + +/// Event to notify the BLoC that the selected content status has changed. +final class FilterLocalAdsStatusChanged extends FilterLocalAdsEvent { + const FilterLocalAdsStatusChanged(this.status); + + final ContentStatus status; + + @override + List get props => [status]; +} + +/// Event to notify the BLoC that the selected ad type has changed. +final class FilterLocalAdsAdTypeChanged extends FilterLocalAdsEvent { + const FilterLocalAdsAdTypeChanged(this.adType); + + final AdType adType; + + @override + List get props => [adType]; +} + +/// Event to request applying all current filters. +final class FilterLocalAdsApplied extends FilterLocalAdsEvent { + const FilterLocalAdsApplied({ + required this.searchQuery, + required this.selectedStatus, + required this.selectedAdType, + }); + + final String searchQuery; + final ContentStatus selectedStatus; + final AdType selectedAdType; + + @override + List get props => [ + searchQuery, + selectedStatus, + selectedAdType, + ]; +} + +/// Event to request resetting all filters to their initial state. +final class FilterLocalAdsReset extends FilterLocalAdsEvent { + const FilterLocalAdsReset(); +} From c372e4f44e9e6250d414eb21a4388506bad0335a Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:56:26 +0100 Subject: [PATCH 09/66] feat(local_ads_management): add FilterLocalAdsState for filter dialog - Define FilterLocalAdsStatus enum to represent operation states - Implement FilterLocalAdsState class with properties for status, exception, search query, selected status, and selected ad type - Include copyWith method for state immutability - Add Equatable props for state comparison --- .../filter_local_ads_state.dart | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_state.dart diff --git a/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_state.dart b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_state.dart new file mode 100644 index 00000000..7123e61f --- /dev/null +++ b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_state.dart @@ -0,0 +1,71 @@ +part of 'filter_local_ads_bloc.dart'; + +/// Represents the status of the filter dialog's operations. +enum FilterLocalAdsStatus { + /// The operation is in its initial state. + initial, + + /// Data is currently being loaded or an operation is in progress. + loading, + + /// Data has been successfully loaded or an operation completed. + success, + + /// An error occurred during data loading or an operation. + failure, +} + +/// {@template filter_local_ads_state} +/// The state for the [FilterLocalAdsBloc]. +/// {@endtemplate} +final class FilterLocalAdsState extends Equatable { + /// {@macro filter_local_ads_state} + const FilterLocalAdsState({ + this.status = FilterLocalAdsStatus.initial, + this.exception, + this.searchQuery = '', + this.selectedStatus = ContentStatus.active, + this.selectedAdType = AdType.native, + }); + + /// The current status of the filter dialog's main operations. + final FilterLocalAdsStatus status; + + /// The exception encountered during a failed operation, if any. + final HttpException? exception; + + /// The current text in the search query field. + final String searchQuery; + + /// The single content status to be included in the filter. + final ContentStatus selectedStatus; + + /// The single ad type to be included in the filter. + final AdType selectedAdType; + + /// Creates a copy of this [FilterLocalAdsState] with updated values. + FilterLocalAdsState copyWith({ + FilterLocalAdsStatus? status, + HttpException? exception, + String? searchQuery, + ContentStatus? selectedStatus, + AdType? selectedAdType, + }) { + return FilterLocalAdsState( + status: status ?? this.status, + exception: exception, + searchQuery: searchQuery ?? this.searchQuery, + selectedStatus: selectedStatus ?? this.selectedStatus, + selectedAdType: selectedAdType ?? this.selectedAdType, + ); + } + + @override + List get props => [ + status, + exception, + searchQuery, + selectedStatus, + selectedAdType, + ]; +} From 9bde6d2117d98651611e1e9f62fb6c21c69a249c Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:56:39 +0100 Subject: [PATCH 10/66] feat(local_ads_management): add banner ads management page - Implement BannerAdsPage for displaying and managing banner ads - Add functionality to load, filter, and paginate banner ads - Include options to copy ad ID, edit, and archive ads - Handle loading, empty, and error states --- .../view/banner_ads_page.dart | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 lib/local_ads_management/view/banner_ads_page.dart diff --git a/lib/local_ads_management/view/banner_ads_page.dart b/lib/local_ads_management/view/banner_ads_page.dart new file mode 100644 index 00000000..6e11a0f1 --- /dev/null +++ b/lib/local_ads_management/view/banner_ads_page.dart @@ -0,0 +1,301 @@ +import 'package:core/core.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template banner_ads_page} +/// A page for displaying and managing Banner Ads in a tabular format. +/// {@endtemplate} +class BannerAdsPage extends StatefulWidget { + /// {@macro banner_ads_page} + const BannerAdsPage({super.key}); + + @override + State createState() => _BannerAdsPageState(); +} + +class _BannerAdsPageState extends State { + @override + void initState() { + super.initState(); + // Initial load of banner ads, applying the default filter from FilterLocalAdsBloc + context.read().add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + filter: context.read().buildLocalAdsFilterMap( + context.read().state.copyWith( + selectedAdType: AdType.banner, + ), + ), + ), + ); + } + + /// Checks if any filters are currently active in the FilterLocalAdsBloc + /// for the banner ad type. + bool _areFiltersActive(FilterLocalAdsState state) { + return state.searchQuery.isNotEmpty || + state.selectedStatus != ContentStatus.active || + state.selectedAdType != AdType.banner; + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: BlocBuilder( + builder: (context, state) { + final filterLocalAdsState = context.watch().state; + final filtersActive = _areFiltersActive(filterLocalAdsState); + + if (state.bannerAdsStatus == LocalAdsManagementStatus.loading && + state.bannerAds.isEmpty) { + return LoadingStateWidget( + icon: Icons.view_carousel, + headline: l10n.loadingBannerAds, + subheadline: l10n.pleaseWait, + ); + } + + if (state.bannerAdsStatus == LocalAdsManagementStatus.failure) { + return FailureStateWidget( + exception: state.exception!, + onRetry: () => context.read().add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildLocalAdsFilterMap( + context.read().state.copyWith( + selectedAdType: AdType.banner, + ), + ), + ), + ), + ); + } + + if (state.bannerAds.isEmpty) { + if (filtersActive) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.noResultsWithCurrentFilters, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.lg), + ElevatedButton( + onPressed: () { + context.read().add( + const FilterLocalAdsReset(), + ); + context.read().add( + const FilterLocalAdsApplied( + searchQuery: '', + selectedStatus: ContentStatus.active, + selectedAdType: AdType.banner, + ), + ); + }, + child: Text(l10n.resetFiltersButtonText), + ), + ], + ), + ); + } + return Center(child: Text(l10n.noBannerAdsFound)); + } + + return Column( + children: [ + if (state.bannerAdsStatus == LocalAdsManagementStatus.loading && + state.bannerAds.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.adImageUrl), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.lastUpdated), + size: ColumnSize.S, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + ), + ], + source: _BannerAdsDataSource( + context: context, + ads: state.bannerAds, + hasMore: state.bannerAdsHasMore, + l10n: l10n, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.bannerAds.length && + state.bannerAdsHasMore && + state.bannerAdsStatus != + LocalAdsManagementStatus.loading) { + context.read().add( + LoadLocalAdsRequested( + startAfterId: state.bannerAdsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildLocalAdsFilterMap( + context + .read() + .state + .copyWith( + selectedAdType: AdType.banner, + ), + ), + ), + ); + } + }, + empty: Center(child: Text(l10n.noBannerAdsFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.md, + horizontalMargin: AppSpacing.md, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _BannerAdsDataSource extends DataTableSource { + _BannerAdsDataSource({ + required this.context, + required this.ads, + required this.hasMore, + required this.l10n, + }); + + final BuildContext context; + final List ads; + final bool hasMore; + final AppLocalizations l10n; + + @override + DataRow? getRow(int index) { + if (index >= ads.length) { + return null; + } + final ad = ads[index]; + + return DataRow2( + cells: [ + DataCell( + Text( + ad.imageUrl.truncate(50), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + DataCell( + Text( + DateFormat('dd-MM-yyyy').format(ad.updatedAt.toLocal()), + ), + ), + DataCell( + Row( + children: [ + // Primary action: Copy ID button + IconButton( + icon: const Icon(Icons.copy), + tooltip: l10n.copyId, + onPressed: () { + Clipboard.setData(ClipboardData(text: ad.id)); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(l10n.idCopiedToClipboard(ad.id)), + ), + ); + }, + ), + // Secondary actions: Edit and Archive via PopupMenuButton + PopupMenuButton( + icon: const Icon(Icons.more_vert), + tooltip: l10n.moreActions, + onSelected: (value) { + if (value == 'edit') { + context.goNamed( + Routes.editLocalBannerAdName, + pathParameters: {'id': ad.id}, + ); + } else if (value == 'archive') { + context.read().add( + ArchiveLocalAdRequested(ad.id), + ); + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + const Icon(Icons.edit), + const SizedBox(width: AppSpacing.sm), + Text(l10n.editLocalAds), + ], + ), + ), + PopupMenuItem( + value: 'archive', + child: Row( + children: [ + const Icon(Icons.archive), + const SizedBox(width: AppSpacing.sm), + Text(l10n.archive), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ); + } + + @override + bool get isRowCountApproximate => hasMore; + + @override + int get rowCount => ads.length; + + @override + int get selectedRowCount => 0; +} From c2af400d1aa3d0402f8b986fb6ac77aef07075ce Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:56:50 +0100 Subject: [PATCH 11/66] feat(local_ads_management): add interstitial ads management page - Implement InterstitialAdsPage for displaying and managing interstitial ads - Add pagination, filtering, and sorting functionality - Include actions for copying ad ID, editing, and archiving ads - Handle loading, empty, and error states - Update UI components for better usability and responsiveness --- .../view/interstitial_ads_page.dart | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 lib/local_ads_management/view/interstitial_ads_page.dart diff --git a/lib/local_ads_management/view/interstitial_ads_page.dart b/lib/local_ads_management/view/interstitial_ads_page.dart new file mode 100644 index 00000000..e5f1aa01 --- /dev/null +++ b/lib/local_ads_management/view/interstitial_ads_page.dart @@ -0,0 +1,302 @@ +import 'package:core/core.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template interstitial_ads_page} +/// A page for displaying and managing Interstitial Ads in a tabular format. +/// {@endtemplate} +class InterstitialAdsPage extends StatefulWidget { + /// {@macro interstitial_ads_page} + const InterstitialAdsPage({super.key}); + + @override + State createState() => _InterstitialAdsPageState(); +} + +class _InterstitialAdsPageState extends State { + @override + void initState() { + super.initState(); + // Initial load of interstitial ads, applying the default filter from FilterLocalAdsBloc + context.read().add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + filter: context.read().buildLocalAdsFilterMap( + context.read().state.copyWith( + selectedAdType: AdType.interstitial, + ), + ), + ), + ); + } + + /// Checks if any filters are currently active in the FilterLocalAdsBloc + /// for the interstitial ad type. + bool _areFiltersActive(FilterLocalAdsState state) { + return state.searchQuery.isNotEmpty || + state.selectedStatus != ContentStatus.active || + state.selectedAdType != AdType.interstitial; + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: BlocBuilder( + builder: (context, state) { + final filterLocalAdsState = context.watch().state; + final filtersActive = _areFiltersActive(filterLocalAdsState); + + if (state.interstitialAdsStatus == LocalAdsManagementStatus.loading && + state.interstitialAds.isEmpty) { + return LoadingStateWidget( + icon: Icons.fullscreen, + headline: l10n.loadingInterstitialAds, + subheadline: l10n.pleaseWait, + ); + } + + if (state.interstitialAdsStatus == LocalAdsManagementStatus.failure) { + return FailureStateWidget( + exception: state.exception!, + onRetry: () => context.read().add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildLocalAdsFilterMap( + context.read().state.copyWith( + selectedAdType: AdType.interstitial, + ), + ), + ), + ), + ); + } + + if (state.interstitialAds.isEmpty) { + if (filtersActive) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.noResultsWithCurrentFilters, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.lg), + ElevatedButton( + onPressed: () { + context.read().add( + const FilterLocalAdsReset(), + ); + context.read().add( + const FilterLocalAdsApplied( + searchQuery: '', + selectedStatus: ContentStatus.active, + selectedAdType: AdType.interstitial, + ), + ); + }, + child: Text(l10n.resetFiltersButtonText), + ), + ], + ), + ); + } + return Center(child: Text(l10n.noInterstitialAdsFound)); + } + + return Column( + children: [ + if (state.interstitialAdsStatus == + LocalAdsManagementStatus.loading && + state.interstitialAds.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.adImageUrl), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.lastUpdated), + size: ColumnSize.S, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + ), + ], + source: _InterstitialAdsDataSource( + context: context, + ads: state.interstitialAds, + hasMore: state.interstitialAdsHasMore, + l10n: l10n, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.interstitialAds.length && + state.interstitialAdsHasMore && + state.interstitialAdsStatus != + LocalAdsManagementStatus.loading) { + context.read().add( + LoadLocalAdsRequested( + startAfterId: state.interstitialAdsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildLocalAdsFilterMap( + context + .read() + .state + .copyWith( + selectedAdType: AdType.interstitial, + ), + ), + ), + ); + } + }, + empty: Center(child: Text(l10n.noInterstitialAdsFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.md, + horizontalMargin: AppSpacing.md, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _InterstitialAdsDataSource extends DataTableSource { + _InterstitialAdsDataSource({ + required this.context, + required this.ads, + required this.hasMore, + required this.l10n, + }); + + final BuildContext context; + final List ads; + final bool hasMore; + final AppLocalizations l10n; + + @override + DataRow? getRow(int index) { + if (index >= ads.length) { + return null; + } + final ad = ads[index]; + + return DataRow2( + cells: [ + DataCell( + Text( + ad.imageUrl.truncate(50), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + DataCell( + Text( + DateFormat('dd-MM-yyyy').format(ad.updatedAt.toLocal()), + ), + ), + DataCell( + Row( + children: [ + // Primary action: Copy ID button + IconButton( + icon: const Icon(Icons.copy), + tooltip: l10n.copyId, + onPressed: () { + Clipboard.setData(ClipboardData(text: ad.id)); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(l10n.idCopiedToClipboard(ad.id)), + ), + ); + }, + ), + // Secondary actions: Edit and Archive via PopupMenuButton + PopupMenuButton( + icon: const Icon(Icons.more_vert), + tooltip: l10n.moreActions, + onSelected: (value) { + if (value == 'edit') { + context.goNamed( + Routes.editLocalInterstitialAdName, + pathParameters: {'id': ad.id}, + ); + } else if (value == 'archive') { + context.read().add( + ArchiveLocalAdRequested(ad.id), + ); + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + const Icon(Icons.edit), + const SizedBox(width: AppSpacing.sm), + Text(l10n.editLocalAds), + ], + ), + ), + PopupMenuItem( + value: 'archive', + child: Row( + children: [ + const Icon(Icons.archive), + const SizedBox(width: AppSpacing.sm), + Text(l10n.archive), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ); + } + + @override + bool get isRowCountApproximate => hasMore; + + @override + int get rowCount => ads.length; + + @override + int get selectedRowCount => 0; +} From 3479fbf6a4bdfc08c24e27fc53965019e8f1ea7b Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:57:02 +0100 Subject: [PATCH 12/66] refactor(local_ads_management): migrate to separate ad type pages - Replace monolithic LocalAdsManagementPage with individual pages for each ad type - Add FilterLocalAdsBloc for handling ad filters - Implement snackbar undo functionality for deleted ads - Update UI to use separate pages for Native, Banner, Interstitial, and Video ads --- .../view/local_ads_management_page.dart | 444 ++---------------- 1 file changed, 50 insertions(+), 394 deletions(-) diff --git a/lib/local_ads_management/view/local_ads_management_page.dart b/lib/local_ads_management/view/local_ads_management_page.dart index 565e67d9..5f7c0d4f 100644 --- a/lib/local_ads_management/view/local_ads_management_page.dart +++ b/lib/local_ads_management/view/local_ads_management_page.dart @@ -1,70 +1,29 @@ -import 'package:core/core.dart'; -import 'package:data_repository/data_repository.dart'; -import 'package:data_table_2/data_table_2.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/banner_ads_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/interstitial_ads_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/native_ads_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/video_ads_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; import 'package:go_router/go_router.dart'; -import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; /// {@template local_ads_management_page} /// A page for managing local advertisements with tabbed navigation for ad types. /// {@endtemplate} -class LocalAdsManagementPage extends StatelessWidget { +class LocalAdsManagementPage extends StatefulWidget { /// {@macro local_ads_management_page} const LocalAdsManagementPage({super.key}); @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - LocalAdsManagementBloc( - localAdsRepository: context.read>(), - ) - ..add( - const LoadLocalAdsRequested( - adType: AdType.native, - limit: kDefaultRowsPerPage, - ), - ) - ..add( - const LoadLocalAdsRequested( - adType: AdType.banner, - limit: kDefaultRowsPerPage, - ), - ) - ..add( - const LoadLocalAdsRequested( - adType: AdType.interstitial, - limit: kDefaultRowsPerPage, - ), - ) - ..add( - const LoadLocalAdsRequested( - adType: AdType.video, - limit: kDefaultRowsPerPage, - ), - ), - child: const _LocalAdsManagementView(), - ); - } + State createState() => _LocalAdsManagementPageState(); } -class _LocalAdsManagementView extends StatefulWidget { - const _LocalAdsManagementView(); - - @override - State<_LocalAdsManagementView> createState() => - _LocalAdsManagementViewState(); -} - -class _LocalAdsManagementViewState extends State<_LocalAdsManagementView> +class _LocalAdsManagementPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; @@ -100,49 +59,31 @@ class _LocalAdsManagementViewState extends State<_LocalAdsManagementView> final l10n = AppLocalizationsX(context).l10n; return BlocListener( listenWhen: (previous, current) => - previous.lastDeletedLocalAd != current.lastDeletedLocalAd, + previous.snackbarMessage != current.snackbarMessage && + current.snackbarMessage != null, listener: (context, state) { - if (state.lastDeletedLocalAd != null) { - String truncatedTitle; - switch (state.lastDeletedLocalAd!.adType) { - case 'native': - truncatedTitle = (state.lastDeletedLocalAd! as LocalNativeAd) - .title - .truncate(30); - case 'banner': - truncatedTitle = (state.lastDeletedLocalAd! as LocalBannerAd) - .imageUrl - .truncate(30); - case 'interstitial': - truncatedTitle = - (state.lastDeletedLocalAd! as LocalInterstitialAd).imageUrl - .truncate(30); - case 'video': - truncatedTitle = (state.lastDeletedLocalAd! as LocalVideoAd) - .videoUrl - .truncate(30); - default: - truncatedTitle = state.lastDeletedLocalAd!.id.truncate(30); - } - - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text( - l10n.localAdDeleted(truncatedTitle), - ), - action: SnackBarAction( - label: l10n.undo, - onPressed: () { - context.read().add( - const UndoDeleteLocalAdRequested(), - ); - }, - ), + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(state.snackbarMessage!), + action: SnackBarAction( + label: l10n.undo, + onPressed: () { + // The snackbar message is set when a deletion is requested. + // The ID of the item to undo is implicitly the one that + // triggered the snackbar. + context.read().add( + UndoDeleteLocalAdRequested( + state.snackbarMessage!.split( + '"', + )[1], // Extract ID from message + ), + ); + }, ), - ); - } + ), + ); }, child: Scaffold( appBar: AppBar( @@ -157,10 +98,20 @@ class _LocalAdsManagementViewState extends State<_LocalAdsManagementView> ), actions: [ IconButton( - icon: const Icon(Icons.inventory_2_outlined), - tooltip: l10n.archivedItems, + icon: const Icon(Icons.filter_list), + tooltip: l10n.filter, onPressed: () { - context.goNamed(Routes.archivedLocalAdsName); + final filterLocalAdsBloc = context.read(); + // Construct arguments map to pass to the filter dialog route + final arguments = { + 'filterLocalAdsBloc': filterLocalAdsBloc, + }; + + // Push the filter dialog as a new route + context.pushNamed( + Routes.localAdsFilterDialogName, + extra: arguments, + ); }, ), IconButton( @@ -188,309 +139,14 @@ class _LocalAdsManagementViewState extends State<_LocalAdsManagementView> ), body: TabBarView( controller: _tabController, - children: LocalAdsManagementTab.values.map((tab) { - return _LocalAdsDataTable(adType: tab); - }).toList(), - ), - ), - ); - } -} - -class _LocalAdsDataTable extends StatelessWidget { - const _LocalAdsDataTable({required this.adType}); - - final LocalAdsManagementTab adType; - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - return BlocBuilder( - builder: (context, state) { - LocalAdsManagementStatus status; - List ads; - String? cursor; - bool hasMore; - - switch (adType) { - case LocalAdsManagementTab.native: - status = state.nativeAdsStatus; - ads = state.nativeAds; - cursor = state.nativeAdsCursor; - hasMore = state.nativeAdsHasMore; - case LocalAdsManagementTab.banner: - status = state.bannerAdsStatus; - ads = state.bannerAds; - cursor = state.bannerAdsCursor; - hasMore = state.bannerAdsHasMore; - case LocalAdsManagementTab.interstitial: - status = state.interstitialAdsStatus; - ads = state.interstitialAds; - cursor = state.interstitialAdsCursor; - hasMore = state.interstitialAdsHasMore; - case LocalAdsManagementTab.video: - status = state.videoAdsStatus; - ads = state.videoAds; - cursor = state.videoAdsCursor; - hasMore = state.videoAdsHasMore; - } - - if (status == LocalAdsManagementStatus.loading && ads.isEmpty) { - return LoadingStateWidget( - icon: Icons.ads_click, - headline: l10n.loadingLocalAds, - subheadline: l10n.pleaseWait, - ); - } - - if (status == LocalAdsManagementStatus.failure) { - return FailureStateWidget( - exception: state.exception!, - onRetry: () => context.read().add( - LoadLocalAdsRequested( - adType: adType.toAdType(), - limit: kDefaultRowsPerPage, - ), - ), - ); - } - - if (ads.isEmpty) { - return Center(child: Text(l10n.noLocalAdsFound)); - } - - return Column( - children: [ - if (status == LocalAdsManagementStatus.loading && ads.isNotEmpty) - const LinearProgressIndicator(), - Expanded( - child: PaginatedDataTable2( - columns: [ - DataColumn2( - label: Text(l10n.adTitle), - size: ColumnSize.L, - ), - DataColumn2( - label: Text(l10n.lastUpdated), - size: ColumnSize.S, - ), - DataColumn2( - label: Text(l10n.actions), - size: ColumnSize.S, - ), - ], - source: _LocalAdsDataSource( - context: context, - ads: ads, - hasMore: hasMore, - l10n: l10n, - adType: adType, - ), - rowsPerPage: kDefaultRowsPerPage, - availableRowsPerPage: const [kDefaultRowsPerPage], - onPageChanged: (pageIndex) { - final newOffset = pageIndex * kDefaultRowsPerPage; - if (newOffset >= ads.length && - hasMore && - status != LocalAdsManagementStatus.loading) { - context.read().add( - LoadLocalAdsRequested( - adType: adType.toAdType(), - startAfterId: cursor, - limit: kDefaultRowsPerPage, - ), - ); - } - }, - empty: Center(child: Text(l10n.noLocalAdsFound)), - showCheckboxColumn: false, - showFirstLastButtons: true, - fit: FlexFit.tight, - headingRowHeight: 56, - dataRowHeight: 56, - columnSpacing: AppSpacing.md, - horizontalMargin: AppSpacing.md, - ), - ), + children: const [ + NativeAdsPage(), + BannerAdsPage(), + InterstitialAdsPage(), + VideoAdsPage(), ], - ); - }, - ); - } -} - -class _LocalAdsDataSource extends DataTableSource { - _LocalAdsDataSource({ - required this.context, - required this.ads, - required this.hasMore, - required this.l10n, - required this.adType, - }); - - final BuildContext context; - final List ads; - final bool hasMore; - final AppLocalizations l10n; - final LocalAdsManagementTab adType; - - @override - DataRow? getRow(int index) { - if (index >= ads.length) { - return null; - } - final ad = ads[index]; - String title; - DateTime updatedAt; - // ignore: unused_local_variable - ContentStatus status; - - // Determine title, updatedAt, and status based on ad type - switch (ad.adType) { - case 'native': - final nativeAd = ad as LocalNativeAd; - title = nativeAd.title; - updatedAt = nativeAd.updatedAt; - status = nativeAd.status; - case 'banner': - final bannerAd = ad as LocalBannerAd; - title = bannerAd.imageUrl; - updatedAt = bannerAd.updatedAt; - status = bannerAd.status; - case 'interstitial': - final interstitialAd = ad as LocalInterstitialAd; - title = interstitialAd.imageUrl; - updatedAt = interstitialAd.updatedAt; - status = interstitialAd.status; - case 'video': - final videoAd = ad as LocalVideoAd; - title = videoAd.videoUrl; - updatedAt = videoAd.updatedAt; - status = videoAd.status; - default: - title = 'Unknown Ad Type'; - updatedAt = DateTime.now(); - status = ContentStatus.active; - } - - return DataRow2( - cells: [ - DataCell( - Text( - title.truncate(50), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - DataCell( - Text( - DateFormat('dd-MM-yyyy').format(updatedAt.toLocal()), - ), - ), - DataCell( - Row( - children: [ - // Primary action: Copy ID button - IconButton( - icon: const Icon(Icons.copy), - tooltip: l10n.copyId, - onPressed: () { - Clipboard.setData(ClipboardData(text: ad.id)); - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text(l10n.idCopiedToClipboard(ad.id)), - ), - ); - }, - ), - // Secondary actions: Edit and Archive via PopupMenuButton - PopupMenuButton( - icon: const Icon(Icons.more_vert), - tooltip: l10n.moreActions, - onSelected: (value) { - if (value == 'edit') { - // Navigate to edit page based on ad type - switch (ad.adType) { - case 'native': - context.goNamed( - Routes.editLocalNativeAdName, - pathParameters: {'id': ad.id}, - ); - case 'banner': - context.goNamed( - Routes.editLocalBannerAdName, - pathParameters: {'id': ad.id}, - ); - case 'interstitial': - context.goNamed( - Routes.editLocalInterstitialAdName, - pathParameters: {'id': ad.id}, - ); - case 'video': - context.goNamed( - Routes.editLocalVideoAdName, - pathParameters: {'id': ad.id}, - ); - } - } else if (value == 'archive') { - context.read().add( - ArchiveLocalAdRequested(ad.id, adType.toAdType()), - ); - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: 'edit', - child: Row( - children: [ - const Icon(Icons.edit), - const SizedBox(width: AppSpacing.sm), - Text(l10n.editLocalAds), - ], - ), - ), - PopupMenuItem( - value: 'archive', - child: Row( - children: [ - const Icon(Icons.archive), - const SizedBox(width: AppSpacing.sm), - Text(l10n.archive), - ], - ), - ), - ], - ), - ], - ), ), - ], + ), ); } - - @override - bool get isRowCountApproximate => hasMore; - - @override - int get rowCount => ads.length; - - @override - int get selectedRowCount => 0; -} - -extension on LocalAdsManagementTab { - AdType toAdType() { - switch (this) { - case LocalAdsManagementTab.native: - return AdType.native; - case LocalAdsManagementTab.banner: - return AdType.banner; - case LocalAdsManagementTab.interstitial: - return AdType.interstitial; - case LocalAdsManagementTab.video: - return AdType.video; - } - } } From 712f5f9bf2f8a068729a29a49996621e07da473c Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:57:11 +0100 Subject: [PATCH 13/66] feat(local_ads_management): implement Native Ads management page - Add NativeAdsPage widget for displaying and managing native ads - Implement pagination and filtering functionality - Include actions for copying ad ID, editing, and archiving ads - Handle loading, empty, and error states - Use BlocBuilder for state management - Add localization support --- .../view/native_ads_page.dart | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 lib/local_ads_management/view/native_ads_page.dart diff --git a/lib/local_ads_management/view/native_ads_page.dart b/lib/local_ads_management/view/native_ads_page.dart new file mode 100644 index 00000000..d488c79d --- /dev/null +++ b/lib/local_ads_management/view/native_ads_page.dart @@ -0,0 +1,251 @@ +import 'package:core/core.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template native_ads_page} +/// A page for displaying and managing Native Ads in a tabular format. +/// {@endtemplate} +class NativeAdsPage extends StatefulWidget { + /// {@macro native_ads_page} + const NativeAdsPage({super.key}); + + @override + State createState() => _NativeAdsPageState(); +} + +class _NativeAdsPageState extends State { + @override + void initState() { + super.initState(); + // Initial load of native ads, applying the default filter from FilterLocalAdsBloc + context.read().add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + filter: context.read().buildLocalAdsFilterMap( + context.read().state, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: BlocBuilder( + builder: (context, state) { + if (state.nativeAdsStatus == LocalAdsManagementStatus.loading && + state.nativeAds.isEmpty) { + return LoadingStateWidget( + icon: Icons.ads_click, + headline: l10n.loadingNativeAds, + subheadline: l10n.pleaseWait, + ); + } + + if (state.nativeAdsStatus == LocalAdsManagementStatus.failure) { + return FailureStateWidget( + exception: state.exception!, + onRetry: () => context.read().add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildLocalAdsFilterMap( + context.read().state, + ), + ), + ), + ); + } + + if (state.nativeAds.isEmpty) { + return Center(child: Text(l10n.noNativeAdsFound)); + } + + return Column( + children: [ + if (state.nativeAdsStatus == LocalAdsManagementStatus.loading && + state.nativeAds.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.adTitle), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.lastUpdated), + size: ColumnSize.S, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + ), + ], + source: _NativeAdsDataSource( + context: context, + ads: state.nativeAds, + hasMore: state.nativeAdsHasMore, + l10n: l10n, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.nativeAds.length && + state.nativeAdsHasMore && + state.nativeAdsStatus != + LocalAdsManagementStatus.loading) { + context.read().add( + LoadLocalAdsRequested( + startAfterId: state.nativeAdsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildLocalAdsFilterMap( + context.read().state, + ), + ), + ); + } + }, + empty: Center(child: Text(l10n.noNativeAdsFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.md, + horizontalMargin: AppSpacing.md, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _NativeAdsDataSource extends DataTableSource { + _NativeAdsDataSource({ + required this.context, + required this.ads, + required this.hasMore, + required this.l10n, + }); + + final BuildContext context; + final List ads; + final bool hasMore; + final AppLocalizations l10n; + + @override + DataRow? getRow(int index) { + if (index >= ads.length) { + return null; + } + final ad = ads[index]; + + return DataRow2( + cells: [ + DataCell( + Text( + ad.title.truncate(50), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + DataCell( + Text( + DateFormat('dd-MM-yyyy').format(ad.updatedAt.toLocal()), + ), + ), + DataCell( + Row( + children: [ + // Primary action: Copy ID button + IconButton( + icon: const Icon(Icons.copy), + tooltip: l10n.copyId, + onPressed: () { + Clipboard.setData(ClipboardData(text: ad.id)); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(l10n.idCopiedToClipboard(ad.id)), + ), + ); + }, + ), + // Secondary actions: Edit and Archive via PopupMenuButton + PopupMenuButton( + icon: const Icon(Icons.more_vert), + tooltip: l10n.moreActions, + onSelected: (value) { + if (value == 'edit') { + context.goNamed( + Routes.editLocalNativeAdName, + pathParameters: {'id': ad.id}, + ); + } else if (value == 'archive') { + context.read().add( + ArchiveLocalAdRequested(ad.id), + ); + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + const Icon(Icons.edit), + const SizedBox(width: AppSpacing.sm), + Text(l10n.editLocalAds), + ], + ), + ), + PopupMenuItem( + value: 'archive', + child: Row( + children: [ + const Icon(Icons.archive), + const SizedBox(width: AppSpacing.sm), + Text(l10n.archive), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ); + } + + @override + bool get isRowCountApproximate => hasMore; + + @override + int get rowCount => ads.length; + + @override + int get selectedRowCount => 0; +} From 01f57010e55a12fab55ee835b7a4af3b59758fc5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:57:22 +0100 Subject: [PATCH 14/66] feat(local_ads_management): add video ads management page - Implement VideoAdsPage for displaying and managing video ads - Add functionality to load, filter, and paginate video ads - Include options to copy ad ID, edit, and archive ads - Handle loading, error, and empty states --- .../view/video_ads_page.dart | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 lib/local_ads_management/view/video_ads_page.dart diff --git a/lib/local_ads_management/view/video_ads_page.dart b/lib/local_ads_management/view/video_ads_page.dart new file mode 100644 index 00000000..98d9d9e6 --- /dev/null +++ b/lib/local_ads_management/view/video_ads_page.dart @@ -0,0 +1,301 @@ +import 'package:core/core.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template video_ads_page} +/// A page for displaying and managing Video Ads in a tabular format. +/// {@endtemplate} +class VideoAdsPage extends StatefulWidget { + /// {@macro video_ads_page} + const VideoAdsPage({super.key}); + + @override + State createState() => _VideoAdsPageState(); +} + +class _VideoAdsPageState extends State { + @override + void initState() { + super.initState(); + // Initial load of video ads, applying the default filter from FilterLocalAdsBloc + context.read().add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + filter: context.read().buildLocalAdsFilterMap( + context.read().state.copyWith( + selectedAdType: AdType.video, + ), + ), + ), + ); + } + + /// Checks if any filters are currently active in the FilterLocalAdsBloc + /// for the video ad type. + bool _areFiltersActive(FilterLocalAdsState state) { + return state.searchQuery.isNotEmpty || + state.selectedStatus != ContentStatus.active || + state.selectedAdType != AdType.video; + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: BlocBuilder( + builder: (context, state) { + final filterLocalAdsState = context.watch().state; + final filtersActive = _areFiltersActive(filterLocalAdsState); + + if (state.videoAdsStatus == LocalAdsManagementStatus.loading && + state.videoAds.isEmpty) { + return LoadingStateWidget( + icon: Icons.videocam, + headline: l10n.loadingVideoAds, + subheadline: l10n.pleaseWait, + ); + } + + if (state.videoAdsStatus == LocalAdsManagementStatus.failure) { + return FailureStateWidget( + exception: state.exception!, + onRetry: () => context.read().add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildLocalAdsFilterMap( + context.read().state.copyWith( + selectedAdType: AdType.video, + ), + ), + ), + ), + ); + } + + if (state.videoAds.isEmpty) { + if (filtersActive) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.noResultsWithCurrentFilters, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.lg), + ElevatedButton( + onPressed: () { + context.read().add( + const FilterLocalAdsReset(), + ); + context.read().add( + const FilterLocalAdsApplied( + searchQuery: '', + selectedStatus: ContentStatus.active, + selectedAdType: AdType.video, + ), + ); + }, + child: Text(l10n.resetFiltersButtonText), + ), + ], + ), + ); + } + return Center(child: Text(l10n.noVideoAdsFound)); + } + + return Column( + children: [ + if (state.videoAdsStatus == LocalAdsManagementStatus.loading && + state.videoAds.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.adVideoUrl), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.lastUpdated), + size: ColumnSize.S, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + ), + ], + source: _VideoAdsDataSource( + context: context, + ads: state.videoAds, + hasMore: state.videoAdsHasMore, + l10n: l10n, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.videoAds.length && + state.videoAdsHasMore && + state.videoAdsStatus != + LocalAdsManagementStatus.loading) { + context.read().add( + LoadLocalAdsRequested( + startAfterId: state.videoAdsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildLocalAdsFilterMap( + context + .read() + .state + .copyWith( + selectedAdType: AdType.video, + ), + ), + ), + ); + } + }, + empty: Center(child: Text(l10n.noVideoAdsFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.md, + horizontalMargin: AppSpacing.md, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _VideoAdsDataSource extends DataTableSource { + _VideoAdsDataSource({ + required this.context, + required this.ads, + required this.hasMore, + required this.l10n, + }); + + final BuildContext context; + final List ads; + final bool hasMore; + final AppLocalizations l10n; + + @override + DataRow? getRow(int index) { + if (index >= ads.length) { + return null; + } + final ad = ads[index]; + + return DataRow2( + cells: [ + DataCell( + Text( + ad.videoUrl.truncate(50), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + DataCell( + Text( + DateFormat('dd-MM-yyyy').format(ad.updatedAt.toLocal()), + ), + ), + DataCell( + Row( + children: [ + // Primary action: Copy ID button + IconButton( + icon: const Icon(Icons.copy), + tooltip: l10n.copyId, + onPressed: () { + Clipboard.setData(ClipboardData(text: ad.id)); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(l10n.idCopiedToClipboard(ad.id)), + ), + ); + }, + ), + // Secondary actions: Edit and Archive via PopupMenuButton + PopupMenuButton( + icon: const Icon(Icons.more_vert), + tooltip: l10n.moreActions, + onSelected: (value) { + if (value == 'edit') { + context.goNamed( + Routes.editLocalVideoAdName, + pathParameters: {'id': ad.id}, + ); + } else if (value == 'archive') { + context.read().add( + ArchiveLocalAdRequested(ad.id), + ); + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + const Icon(Icons.edit), + const SizedBox(width: AppSpacing.sm), + Text(l10n.editLocalAds), + ], + ), + ), + PopupMenuItem( + value: 'archive', + child: Row( + children: [ + const Icon(Icons.archive), + const SizedBox(width: AppSpacing.sm), + Text(l10n.archive), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ); + } + + @override + bool get isRowCountApproximate => hasMore; + + @override + int get rowCount => ads.length; + + @override + int get selectedRowCount => 0; +} From 272b961f5c8b42a33836cbe9f6bf70d43c31136e Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:57:43 +0100 Subject: [PATCH 15/66] feat(local_ads_management): add local ads filter dialog - Implement LocalAdsFilterDialog widget for applying filters to local ads lists - Add BlocBuilder for LocalAdsFilterDialogBloc to manage filter states - Include search functionality and filter chips for content status and ad type - Implement reset and apply filters actions --- .../local_ads_filter_dialog.dart | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 lib/local_ads_management/widgets/local_ads_filter_dialog/local_ads_filter_dialog.dart diff --git a/lib/local_ads_management/widgets/local_ads_filter_dialog/local_ads_filter_dialog.dart b/lib/local_ads_management/widgets/local_ads_filter_dialog/local_ads_filter_dialog.dart new file mode 100644 index 00000000..2987cd6c --- /dev/null +++ b/lib/local_ads_management/widgets/local_ads_filter_dialog/local_ads_filter_dialog.dart @@ -0,0 +1,222 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/ad_type_l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/content_status_l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template local_ads_filter_dialog} +/// A full-screen dialog for applying filters to local ads lists. +/// +/// This dialog provides a search text field and filter chips for content status +/// and ad type. +/// {@endtemplate} +class LocalAdsFilterDialog extends StatefulWidget { + /// {@macro local_ads_filter_dialog} + const LocalAdsFilterDialog({ + required this.filterLocalAdsBloc, + super.key, + }); + + /// The [FilterLocalAdsBloc] instance to interact with. + final FilterLocalAdsBloc filterLocalAdsBloc; + + @override + State createState() => _LocalAdsFilterDialogState(); +} + +class _LocalAdsFilterDialogState extends State { + late TextEditingController _searchController; + + @override + void initState() { + super.initState(); + _searchController = TextEditingController(); + // Initialize the LocalAdsFilterDialogBloc with current filter states. + context.read().add( + LocalAdsFilterDialogInitialized( + filterLocalAdsState: widget.filterLocalAdsBloc.state, + ), + ); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final theme = Theme.of(context); + + return BlocBuilder( + builder: (context, filterDialogState) { + _searchController.text = filterDialogState.searchQuery; + return Scaffold( + appBar: AppBar( + title: Text(l10n.filterLocalAds), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + tooltip: l10n.resetFiltersButtonText, + onPressed: () { + // Dispatch reset event + context.read().add( + const LocalAdsFilterDialogReset(), + ); + // After reset, get the new state and apply filters + const resetState = FilterLocalAdsState(); + widget.filterLocalAdsBloc.add( + FilterLocalAdsApplied( + searchQuery: resetState.searchQuery, + selectedStatus: resetState.selectedStatus, + selectedAdType: resetState.selectedAdType, + ), + ); + Navigator.of(context).pop(); + }, + ), + IconButton( + icon: const Icon(Icons.check), + tooltip: l10n.applyFilters, + onPressed: () { + widget.filterLocalAdsBloc.add( + FilterLocalAdsApplied( + searchQuery: filterDialogState.searchQuery, + selectedStatus: filterDialogState.selectedStatus, + selectedAdType: filterDialogState.selectedAdType, + ), + ); + Navigator.of(context).pop(); + }, + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _searchController, + decoration: InputDecoration( + labelText: l10n.search, + hintText: l10n.searchByAdTitleOrUrl, + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + ), + onChanged: (query) { + context.read().add( + LocalAdsFilterDialogSearchQueryChanged(query), + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + Text( + l10n.status, + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.sm), + _buildStatusFilterChips(l10n, theme, filterDialogState), + const SizedBox(height: AppSpacing.lg), + Text( + l10n.adType, + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.sm), + _buildAdTypeFilterChips(l10n, theme, filterDialogState), + ], + ), + ), + ), + ); + }, + ); + } + + /// Builds the status filter chips. + Widget _buildStatusFilterChips( + AppLocalizations l10n, + ThemeData theme, + LocalAdsFilterDialogState filterDialogState, + ) { + return Wrap( + spacing: AppSpacing.sm, + children: ContentStatus.values.map((status) { + return ChoiceChip( + label: Text(status.l10n(context)), + selected: filterDialogState.selectedStatus == status, + onSelected: (isSelected) { + if (isSelected) { + context.read().add( + LocalAdsFilterDialogStatusChanged(status), + ); + } + }, + selectedColor: theme.colorScheme.primaryContainer, + labelStyle: TextStyle( + color: filterDialogState.selectedStatus == status + ? theme.colorScheme.onPrimaryContainer + : theme.colorScheme.onSurface, + ), + ); + }).toList(), + ); + } + + /// Builds the ad type filter chips. + Widget _buildAdTypeFilterChips( + AppLocalizations l10n, + ThemeData theme, + LocalAdsFilterDialogState filterDialogState, + ) { + return Wrap( + spacing: AppSpacing.sm, + children: AdType.values.map((adType) { + return ChoiceChip( + avatar: Icon(_getAdTypeIcon(adType)), + label: Text(adType.l10n(context)), + selected: filterDialogState.selectedAdType == adType, + onSelected: (isSelected) { + if (isSelected) { + context.read().add( + LocalAdsFilterDialogAdTypeChanged(adType), + ); + } + }, + selectedColor: theme.colorScheme.primaryContainer, + labelStyle: TextStyle( + color: filterDialogState.selectedAdType == adType + ? theme.colorScheme.onPrimaryContainer + : theme.colorScheme.onSurface, + ), + ); + }).toList(), + ); + } + + /// Returns the appropriate icon for a given AdType. + IconData _getAdTypeIcon(AdType adType) { + switch (adType) { + case AdType.native: + return Icons.article; + case AdType.banner: + return Icons.view_carousel; + case AdType.interstitial: + return Icons.fullscreen; + case AdType.video: + return Icons.videocam; + } + } +} From 1aaa841619523c08760e9e86a664c7024bcd4744 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:57:56 +0100 Subject: [PATCH 16/66] feat(local_ads_management): implement LocalAdsFilterDialogBloc - Add new BLoC to manage state and logic for LocalAdsFilterDialog - Implement event handlers for filter initialization, search query change, status change, ad type change, and reset - Use debounce transformer for search query changes - Update state based on current filter selections --- .../bloc/local_ads_filter_dialog_bloc.dart | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart diff --git a/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart b/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart new file mode 100644 index 00000000..db2e7595 --- /dev/null +++ b/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart @@ -0,0 +1,88 @@ +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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ads_filter_dialog/local_ads_filter_dialog.dart' + show LocalAdsFilterDialog; +import 'package:rxdart/rxdart.dart'; + +part 'local_ads_filter_dialog_event.dart'; +part 'local_ads_filter_dialog_state.dart'; + +/// A transformer to debounce events, typically used for search input. +EventTransformer debounce(Duration duration) { + return (events, mapper) => events.debounceTime(duration).flatMap(mapper); +} + +/// {@template local_ads_filter_dialog_bloc} +/// A BLoC that manages the state and logic for the [LocalAdsFilterDialog]. +/// +/// This BLoC handles the temporary filter selections and provides +/// the necessary state for the UI to render the filter dialog. +/// {@endtemplate} +class LocalAdsFilterDialogBloc + extends Bloc { + /// {@macro local_ads_filter_dialog_bloc} + LocalAdsFilterDialogBloc({ + required FilterLocalAdsBloc filterLocalAdsBloc, + }) : _filterLocalAdsBloc = filterLocalAdsBloc, + super(const LocalAdsFilterDialogState()) { + on(_onLocalAdsFilterDialogInitialized); + on( + _onLocalAdsFilterDialogSearchQueryChanged, + transformer: debounce(const Duration(milliseconds: 300)), + ); + on(_onLocalAdsFilterDialogStatusChanged); + on(_onLocalAdsFilterDialogAdTypeChanged); + on(_onLocalAdsFilterDialogReset); + } + + final FilterLocalAdsBloc _filterLocalAdsBloc; + + /// Initializes the filter dialog's state from the current filter BLoC. + void _onLocalAdsFilterDialogInitialized( + LocalAdsFilterDialogInitialized event, + Emitter emit, + ) { + final filterState = event.filterLocalAdsState; + emit( + state.copyWith( + searchQuery: filterState.searchQuery, + selectedStatus: filterState.selectedStatus, + selectedAdType: filterState.selectedAdType, + ), + ); + } + + /// Updates the temporary search query. + void _onLocalAdsFilterDialogSearchQueryChanged( + LocalAdsFilterDialogSearchQueryChanged event, + Emitter emit, + ) { + emit(state.copyWith(searchQuery: event.query)); + } + + /// Updates the temporary selected content status. + void _onLocalAdsFilterDialogStatusChanged( + LocalAdsFilterDialogStatusChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedStatus: event.status)); + } + + /// Updates the temporary selected ad type. + void _onLocalAdsFilterDialogAdTypeChanged( + LocalAdsFilterDialogAdTypeChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedAdType: event.adType)); + } + + /// Resets all temporary filter selections in the dialog to their initial state. + void _onLocalAdsFilterDialogReset( + LocalAdsFilterDialogReset event, + Emitter emit, + ) { + emit(const LocalAdsFilterDialogState()); + } +} From e9b95dc8e1ae5d5855cc0cc166d52739c2623189 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:58:07 +0100 Subject: [PATCH 17/66] feat(local_ads_management): add LocalAdsFilterDialogBloc events - Define sealed class LocalAdsFilterDialogEvent - Add LocalAdsFilterDialogInitialized event for filter dialog initialization - Implement LocalAdsFilterDialogSearchQueryChanged for updating search query - Create LocalAdsFilterDialogStatusChanged for changing content status - Add LocalAdsFilterDialogAdTypeChanged event for selecting ad type - Include LocalAdsFilterDialogReset to reset filter selections --- .../bloc/local_ads_filter_dialog_event.dart | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_event.dart diff --git a/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_event.dart b/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_event.dart new file mode 100644 index 00000000..2fb8bf07 --- /dev/null +++ b/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_event.dart @@ -0,0 +1,61 @@ +part of 'local_ads_filter_dialog_bloc.dart'; + +sealed class LocalAdsFilterDialogEvent extends Equatable { + const LocalAdsFilterDialogEvent(); + + @override + List get props => []; +} + +/// Event to initialize the filter dialog's state from the current filter BLoCs. +final class LocalAdsFilterDialogInitialized extends LocalAdsFilterDialogEvent { + const LocalAdsFilterDialogInitialized({ + required this.filterLocalAdsState, + }); + + final FilterLocalAdsState filterLocalAdsState; + + @override + List get props => [ + filterLocalAdsState, + ]; +} + +/// Event to update the temporary search query. +final class LocalAdsFilterDialogSearchQueryChanged + extends LocalAdsFilterDialogEvent { + const LocalAdsFilterDialogSearchQueryChanged(this.query); + + final String query; + + @override + List get props => [query]; +} + +/// Event to update the temporary selected content status. +final class LocalAdsFilterDialogStatusChanged + extends LocalAdsFilterDialogEvent { + const LocalAdsFilterDialogStatusChanged(this.status); + + final ContentStatus status; + + @override + List get props => [status]; +} + +/// Event to update the temporary selected ad type. +final class LocalAdsFilterDialogAdTypeChanged + extends LocalAdsFilterDialogEvent { + const LocalAdsFilterDialogAdTypeChanged(this.adType); + + final AdType adType; + + @override + List get props => [adType]; +} + +/// Event to reset all temporary filter selections in the dialog to their +/// initial state. +final class LocalAdsFilterDialogReset extends LocalAdsFilterDialogEvent { + const LocalAdsFilterDialogReset(); +} From 8a5e1741b46575860f07eb29d0d1afd017ca0828 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:58:16 +0100 Subject: [PATCH 18/66] feat(local_ads_management): add filter dialog state - Define LocalAdsFilterDialogStatus enum for dialog operation states - Implement LocalAdsFilterDialogState class with properties for status, exception, search query, selected status, and selected ad type - Include copyWith method for creating updated state instances - Add Equatable implementation for state comparison --- .../bloc/local_ads_filter_dialog_state.dart | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_state.dart diff --git a/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_state.dart b/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_state.dart new file mode 100644 index 00000000..ee6ffdc7 --- /dev/null +++ b/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_state.dart @@ -0,0 +1,71 @@ +part of 'local_ads_filter_dialog_bloc.dart'; + +/// Represents the status of the filter dialog's operations. +enum LocalAdsFilterDialogStatus { + /// The operation is in its initial state. + initial, + + /// Data is currently being loaded or an operation is in progress. + loading, + + /// Data has been successfully loaded or an operation completed. + success, + + /// An error occurred during data loading or an operation. + failure, +} + +/// {@template local_ads_filter_dialog_state} +/// The state for the [LocalAdsFilterDialogBloc]. +/// {@endtemplate} +final class LocalAdsFilterDialogState extends Equatable { + /// {@macro local_ads_filter_dialog_state} + const LocalAdsFilterDialogState({ + this.status = LocalAdsFilterDialogStatus.initial, + this.exception, + this.searchQuery = '', + this.selectedStatus = ContentStatus.active, + this.selectedAdType = AdType.native, + }); + + /// The current status of the filter dialog's main operations. + final LocalAdsFilterDialogStatus status; + + /// The exception encountered during a failed operation, if any. + final HttpException? exception; + + /// The current text in the search query field. + final String searchQuery; + + /// The single content status to be included in the filter. + final ContentStatus selectedStatus; + + /// The single ad type to be included in the filter. + final AdType selectedAdType; + + /// Creates a copy of this [LocalAdsFilterDialogState] with updated values. + LocalAdsFilterDialogState copyWith({ + LocalAdsFilterDialogStatus? status, + HttpException? exception, + String? searchQuery, + ContentStatus? selectedStatus, + AdType? selectedAdType, + }) { + return LocalAdsFilterDialogState( + status: status ?? this.status, + exception: exception, + searchQuery: searchQuery ?? this.searchQuery, + selectedStatus: selectedStatus ?? this.selectedStatus, + selectedAdType: selectedAdType ?? this.selectedAdType, + ); + } + + @override + List get props => [ + status, + exception, + searchQuery, + selectedStatus, + selectedAdType, + ]; +} From 01fc52250adc8dea1f5df384cf5e68c5c9ee2819 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:58:51 +0100 Subject: [PATCH 19/66] feat(local_ads_management): add FilterLocalAdsBloc to app providers - Import FilterLocalAdsBloc from local_ads_management/bloc - Add BlocProvider for FilterLocalAdsBloc in the app's provider list --- lib/app/view/app.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 7ed3c7a1..8506a2ee 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -16,6 +16,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/content_manageme import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/sources_filter/sources_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/topics_filter/topics_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/overview/bloc/overview_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/router.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; @@ -131,6 +132,9 @@ class App extends StatelessWidget { BlocProvider( create: (context) => SourcesFilterBloc(), ), + BlocProvider( + create: (context) => FilterLocalAdsBloc(), + ), BlocProvider( create: (context) => ContentManagementBloc( headlinesRepository: context.read>(), From 0ce6b2ac2a784a256d892b4788a6f723e84d0f44 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 11:59:19 +0100 Subject: [PATCH 20/66] feat(router): add local ads filter dialog route - Replace archived local ads page route with a more generic local ads filter dialog route - Update import statements to include new bloc and dialog classes - Add new route for local ads filter dialog with page builder - Update routes.dart to reflect changes in route names and paths --- lib/router/router.dart | 33 ++++++++++++++++++++++++++++----- lib/router/routes.dart | 8 ++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/router/router.dart b/lib/router/router.dart index 8cf58262..637eb7b2 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -25,7 +25,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/view/edit_topic_page.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/content_management/widgets/filter_dialog/filter_dialog.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/archived_local_ads_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/create_local_banner_ad_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/create_local_interstitial_ad_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/create_local_native_ad_page.dart'; @@ -35,6 +35,8 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_manage import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/edit_local_native_ad_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/edit_local_video_ad_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/local_ads_management_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ads_filter_dialog/local_ads_filter_dialog.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/overview/view/overview_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/settings/view/settings_page.dart'; @@ -305,10 +307,31 @@ GoRouter createRouter({ builder: (context, state) => const LocalAdsManagementPage(), routes: [ GoRoute( - path: Routes.archivedLocalAds, - name: Routes.archivedLocalAdsName, - builder: (context, state) => - const ArchivedLocalAdsPage(), + path: Routes.localAdsFilterDialog, + name: Routes.localAdsFilterDialogName, + pageBuilder: (context, state) { + final args = state.extra! as Map; + final filterLocalAdsBloc = + args['filterLocalAdsBloc'] as FilterLocalAdsBloc; + + return MaterialPage( + fullscreenDialog: true, + child: BlocProvider( + create: (providerContext) => + LocalAdsFilterDialogBloc( + filterLocalAdsBloc: filterLocalAdsBloc, + )..add( + LocalAdsFilterDialogInitialized( + filterLocalAdsState: + filterLocalAdsBloc.state, + ), + ), + child: LocalAdsFilterDialog( + filterLocalAdsBloc: filterLocalAdsBloc, + ), + ), + ); + }, ), GoRoute( path: Routes.createLocalNativeAd, diff --git a/lib/router/routes.dart b/lib/router/routes.dart index 1b6ae0f5..4971d9b1 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -110,11 +110,11 @@ abstract final class Routes { /// The name for the local ads management page route. static const String localAdsManagementName = 'localAdsManagement'; - /// The path for the archived local ads page. - static const String archivedLocalAds = 'archived-local-ads'; + /// The path for the generic searchable selection page. + static const String localAdsFilterDialog = 'local-ads-filter-dialog'; - /// The name for the archived local ads page route. - static const String archivedLocalAdsName = 'archivedLocalAds'; + /// The name for the generic searchable selection page route. + static const String localAdsFilterDialogName = 'localAdsFilterDialog'; /// The path for creating a new local native ad. static const String createLocalNativeAd = 'create-local-native-ad'; From e02a15e0963ea715bb9d0788cfbbceed72fba3c8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 14:56:09 +0100 Subject: [PATCH 21/66] chore: delete absolete files --- .../archive_local_ads_bloc.dart | 563 ------------------ .../archive_local_ads_event.dart | 84 --- .../archive_local_ads_state.dart | 171 ------ .../view/archived_local_ads_page.dart | 364 ----------- 4 files changed, 1182 deletions(-) delete mode 100644 lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_bloc.dart delete mode 100644 lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_event.dart delete mode 100644 lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_state.dart delete mode 100644 lib/local_ads_management/view/archived_local_ads_page.dart diff --git a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_bloc.dart b/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_bloc.dart deleted file mode 100644 index 6e01c649..00000000 --- a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_bloc.dart +++ /dev/null @@ -1,563 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:core/core.dart'; -import 'package:data_repository/data_repository.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; - -part 'archive_local_ads_event.dart'; -part 'archive_local_ads_state.dart'; - -/// {@template archive_local_ads_bloc} -/// A BLoC responsible for managing the state of archived local ads. -/// -/// It handles loading, restoring, and permanently deleting archived local ads, -/// leveraging the [PendingDeletionsService] for undo functionality. -/// {@endtemplate} -class ArchiveLocalAdsBloc - extends Bloc { - /// {@macro archive_local_ads_bloc} - ArchiveLocalAdsBloc({ - required DataRepository localAdsRepository, - required PendingDeletionsService pendingDeletionsService, - }) : _localAdsRepository = localAdsRepository, - _pendingDeletionsService = pendingDeletionsService, - super(const ArchiveLocalAdsState()) { - on(_onLoadArchivedLocalAdsRequested); - on(_onRestoreLocalAdRequested); - on(_onDeleteLocalAdForeverRequested); - on(_onUndoDeleteLocalAdRequested); - on<_DeletionServiceStatusChanged>(_onDeletionServiceStatusChanged); - - // Listen to deletion events from the PendingDeletionsService. - _deletionEventSubscription = _pendingDeletionsService.deletionEvents.listen( - (event) { - if (event.item is LocalAd) { - add(_DeletionServiceStatusChanged(event)); - } - }, - ); - } - - final DataRepository _localAdsRepository; - final PendingDeletionsService _pendingDeletionsService; - - /// Subscription to deletion events from the PendingDeletionsService. - late final StreamSubscription> - _deletionEventSubscription; - - @override - Future close() { - _deletionEventSubscription.cancel(); - return super.close(); - } - - /// Handles the request to load archived local ads for a specific type. - /// - /// Fetches paginated archived local ads from the repository and updates the state. - Future _onLoadArchivedLocalAdsRequested( - LoadArchivedLocalAdsRequested event, - Emitter emit, - ) async { - // Determine current state and emit loading status - switch (event.adType) { - case AdType.native: - emit(state.copyWith(nativeAdsStatus: ArchiveLocalAdsStatus.loading)); - case AdType.banner: - emit(state.copyWith(bannerAdsStatus: ArchiveLocalAdsStatus.loading)); - case AdType.interstitial: - emit( - state.copyWith( - interstitialAdsStatus: ArchiveLocalAdsStatus.loading, - ), - ); - case AdType.video: - emit(state.copyWith(videoAdsStatus: ArchiveLocalAdsStatus.loading)); - } - - try { - final isPaginating = event.startAfterId != null; - final paginatedAds = await _localAdsRepository.readAll( - filter: { - 'adType': event.adType.name, - 'status': ContentStatus.archived.name, - }, - sort: [const SortOption('updatedAt', SortOrder.desc)], - pagination: PaginationOptions( - cursor: event.startAfterId, - limit: event.limit, - ), - ); - - switch (event.adType) { - case AdType.native: - final previousAds = isPaginating - ? state.nativeAds - : []; - emit( - state.copyWith( - nativeAdsStatus: ArchiveLocalAdsStatus.success, - nativeAds: [ - ...previousAds, - ...paginatedAds.items.cast(), - ], - nativeAdsCursor: paginatedAds.cursor, - nativeAdsHasMore: paginatedAds.hasMore, - ), - ); - case AdType.banner: - final previousAds = isPaginating - ? state.bannerAds - : []; - emit( - state.copyWith( - bannerAdsStatus: ArchiveLocalAdsStatus.success, - bannerAds: [ - ...previousAds, - ...paginatedAds.items.cast(), - ], - bannerAdsCursor: paginatedAds.cursor, - bannerAdsHasMore: paginatedAds.hasMore, - ), - ); - case AdType.interstitial: - final previousAds = isPaginating - ? state.interstitialAds - : []; - emit( - state.copyWith( - interstitialAdsStatus: ArchiveLocalAdsStatus.success, - interstitialAds: [ - ...previousAds, - ...paginatedAds.items.cast(), - ], - interstitialAdsCursor: paginatedAds.cursor, - interstitialAdsHasMore: paginatedAds.hasMore, - ), - ); - case AdType.video: - final previousAds = isPaginating ? state.videoAds : []; - emit( - state.copyWith( - videoAdsStatus: ArchiveLocalAdsStatus.success, - videoAds: [ - ...previousAds, - ...paginatedAds.items.cast(), - ], - videoAdsCursor: paginatedAds.cursor, - videoAdsHasMore: paginatedAds.hasMore, - ), - ); - } - } on HttpException catch (e) { - switch (event.adType) { - case AdType.native: - emit( - state.copyWith( - nativeAdsStatus: ArchiveLocalAdsStatus.failure, - exception: e, - ), - ); - case AdType.banner: - emit( - state.copyWith( - bannerAdsStatus: ArchiveLocalAdsStatus.failure, - exception: e, - ), - ); - case AdType.interstitial: - emit( - state.copyWith( - interstitialAdsStatus: ArchiveLocalAdsStatus.failure, - exception: e, - ), - ); - case AdType.video: - emit( - state.copyWith( - videoAdsStatus: ArchiveLocalAdsStatus.failure, - exception: e, - ), - ); - } - } catch (e) { - switch (event.adType) { - case AdType.native: - emit( - state.copyWith( - nativeAdsStatus: ArchiveLocalAdsStatus.failure, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - case AdType.banner: - emit( - state.copyWith( - bannerAdsStatus: ArchiveLocalAdsStatus.failure, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - case AdType.interstitial: - emit( - state.copyWith( - interstitialAdsStatus: ArchiveLocalAdsStatus.failure, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - case AdType.video: - emit( - state.copyWith( - videoAdsStatus: ArchiveLocalAdsStatus.failure, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - } - } - } - - /// Handles the request to restore an archived local ad. - /// - /// Optimistically removes the ad from the UI, updates its status to active - /// in the repository, and then updates the state. If the ad was pending - /// deletion, its pending deletion is cancelled. - Future _onRestoreLocalAdRequested( - RestoreLocalAdRequested event, - Emitter emit, - ) async { - // Cancel any pending deletion for this ad. - _pendingDeletionsService.undoDeletion(event.id); - - LocalAd? adToRestore; - final originalNativeAds = List.from(state.nativeAds); - final originalBannerAds = List.from(state.bannerAds); - final originalInterstitialAds = List.from( - state.interstitialAds, - ); - final originalVideoAds = List.from(state.videoAds); - - switch (event.adType) { - case AdType.native: - final index = originalNativeAds.indexWhere((ad) => ad.id == event.id); - if (index == -1) return; - adToRestore = originalNativeAds[index]; - originalNativeAds.removeAt(index); - emit( - state.copyWith( - nativeAds: originalNativeAds, - lastPendingDeletionId: state.lastPendingDeletionId == event.id - ? null - : state.lastPendingDeletionId, - snackbarLocalAdTitle: null, - ), - ); - case AdType.banner: - final index = originalBannerAds.indexWhere((ad) => ad.id == event.id); - if (index == -1) return; - adToRestore = originalBannerAds[index]; - originalBannerAds.removeAt(index); - emit( - state.copyWith( - bannerAds: originalBannerAds, - lastPendingDeletionId: state.lastPendingDeletionId == event.id - ? null - : state.lastPendingDeletionId, - snackbarLocalAdTitle: null, - ), - ); - case AdType.interstitial: - final index = originalInterstitialAds.indexWhere( - (ad) => ad.id == event.id, - ); - if (index == -1) return; - adToRestore = originalInterstitialAds[index]; - originalInterstitialAds.removeAt(index); - emit( - state.copyWith( - interstitialAds: originalInterstitialAds, - lastPendingDeletionId: state.lastPendingDeletionId == event.id - ? null - : state.lastPendingDeletionId, - snackbarLocalAdTitle: null, - ), - ); - case AdType.video: - final index = originalVideoAds.indexWhere((ad) => ad.id == event.id); - if (index == -1) return; - adToRestore = originalVideoAds[index]; - originalVideoAds.removeAt(index); - emit( - state.copyWith( - videoAds: originalVideoAds, - lastPendingDeletionId: state.lastPendingDeletionId == event.id - ? null - : state.lastPendingDeletionId, - snackbarLocalAdTitle: null, - ), - ); - } - - try { - final updatedAd = switch (adToRestore) { - final LocalNativeAd ad => ad.copyWith(status: ContentStatus.active), - final LocalBannerAd ad => ad.copyWith(status: ContentStatus.active), - final LocalInterstitialAd ad => ad.copyWith( - status: ContentStatus.active, - ), - final LocalVideoAd ad => ad.copyWith(status: ContentStatus.active), - _ => throw StateError( - 'Unknown LocalAd type: ${adToRestore.runtimeType}', - ), - }; - await _localAdsRepository.update( - id: event.id, - item: updatedAd, - ); - } on HttpException catch (e) { - // Revert UI on failure - switch (event.adType) { - case AdType.native: - emit( - state.copyWith( - nativeAds: originalNativeAds, - exception: e, - lastPendingDeletionId: state.lastPendingDeletionId, - ), - ); - case AdType.banner: - emit( - state.copyWith( - bannerAds: originalBannerAds, - exception: e, - lastPendingDeletionId: state.lastPendingDeletionId, - ), - ); - case AdType.interstitial: - emit( - state.copyWith( - interstitialAds: originalInterstitialAds, - exception: e, - lastPendingDeletionId: state.lastPendingDeletionId, - ), - ); - case AdType.video: - emit( - state.copyWith( - videoAds: originalVideoAds, - exception: e, - lastPendingDeletionId: state.lastPendingDeletionId, - ), - ); - } - } catch (e) { - switch (event.adType) { - case AdType.native: - emit( - state.copyWith( - nativeAds: originalNativeAds, - exception: UnknownException('An unexpected error occurred: $e'), - lastPendingDeletionId: state.lastPendingDeletionId, - ), - ); - case AdType.banner: - emit( - state.copyWith( - bannerAds: originalBannerAds, - exception: UnknownException('An unexpected error occurred: $e'), - lastPendingDeletionId: state.lastPendingDeletionId, - ), - ); - case AdType.interstitial: - emit( - state.copyWith( - interstitialAds: originalInterstitialAds, - exception: UnknownException('An unexpected error occurred: $e'), - lastPendingDeletionId: state.lastPendingDeletionId, - ), - ); - case AdType.video: - emit( - state.copyWith( - videoAds: originalVideoAds, - exception: UnknownException('An unexpected error occurred: $e'), - lastPendingDeletionId: state.lastPendingDeletionId, - ), - ); - } - } - } - - /// Handles the request to permanently delete an archived local ad. - /// - /// This optimistically removes the ad from the UI and initiates a - /// timed deletion via the [PendingDeletionsService]. - Future _onDeleteLocalAdForeverRequested( - DeleteLocalAdForeverRequested event, - Emitter emit, - ) async { - LocalAd? adToDelete; - final currentNativeAds = List.from(state.nativeAds); - final currentBannerAds = List.from(state.bannerAds); - final currentInterstitialAds = List.from( - state.interstitialAds, - ); - final currentVideoAds = List.from(state.videoAds); - - // Find and remove the ad from the current active list - var index = -1; - switch (event.adType) { - case AdType.native: - index = currentNativeAds.indexWhere((ad) => ad.id == event.id); - if (index != -1) adToDelete = currentNativeAds[index]; - case AdType.banner: - index = currentBannerAds.indexWhere((ad) => ad.id == event.id); - if (index != -1) adToDelete = currentBannerAds[index]; - case AdType.interstitial: - index = currentInterstitialAds.indexWhere((ad) => ad.id == event.id); - if (index != -1) adToDelete = currentInterstitialAds[index]; - case AdType.video: - index = currentVideoAds.indexWhere((ad) => ad.id == event.id); - if (index != -1) adToDelete = currentVideoAds[index]; - } - - if (adToDelete == null) return; - - // Optimistically remove from UI - switch (adToDelete.adType) { - case 'native': - currentNativeAds.removeWhere((ad) => ad.id == event.id); - emit(state.copyWith(nativeAds: currentNativeAds)); - case 'banner': - currentBannerAds.removeWhere((ad) => ad.id == event.id); - emit(state.copyWith(bannerAds: currentBannerAds)); - case 'interstitial': - currentInterstitialAds.removeWhere((ad) => ad.id == event.id); - emit(state.copyWith(interstitialAds: currentInterstitialAds)); - case 'video': - currentVideoAds.removeWhere((ad) => ad.id == event.id); - emit(state.copyWith(videoAds: currentVideoAds)); - } - - String snackbarTitle; - switch (adToDelete.adType) { - case 'native': - snackbarTitle = (adToDelete as LocalNativeAd).title; - case 'banner': - snackbarTitle = (adToDelete as LocalBannerAd).imageUrl; - case 'interstitial': - snackbarTitle = (adToDelete as LocalInterstitialAd).imageUrl; - case 'video': - snackbarTitle = (adToDelete as LocalVideoAd).videoUrl; - default: - snackbarTitle = adToDelete.id; - } - - emit( - state.copyWith( - lastPendingDeletionId: event.id, - snackbarLocalAdTitle: snackbarTitle, - ), - ); - - // Request deletion via the service. - _pendingDeletionsService.requestDeletion( - item: adToDelete, - repository: _localAdsRepository, - undoDuration: const Duration(seconds: 5), - ); - } - - /// Handles the request to undo a pending deletion of an archived local ad. - /// - /// This cancels the deletion timer in the [PendingDeletionsService]. - Future _onUndoDeleteLocalAdRequested( - UndoDeleteLocalAdRequested event, - Emitter emit, - ) async { - if (state.lastPendingDeletionId != null) { - _pendingDeletionsService.undoDeletion(state.lastPendingDeletionId!); - } - // The _onDeletionServiceStatusChanged will handle re-adding to the list - // and updating pendingDeletions when DeletionStatus.undone is emitted. - } - - /// Handles deletion events from the [PendingDeletionsService]. - /// - /// This method is called when an item's deletion is confirmed or undone - /// by the service. It updates the BLoC's state accordingly. - Future _onDeletionServiceStatusChanged( - _DeletionServiceStatusChanged event, - Emitter emit, - ) async { - final id = event.event.id; - final status = event.event.status; - final item = event.event.item; - - if (status == DeletionStatus.confirmed) { - // Deletion confirmed, no action needed in BLoC as it was optimistically removed. - // Ensure lastPendingDeletionId and snackbarLocalAdTitle are cleared if this was the one. - emit( - state.copyWith( - lastPendingDeletionId: state.lastPendingDeletionId == id - ? null - : state.lastPendingDeletionId, - snackbarLocalAdTitle: null, - ), - ); - } else if (status == DeletionStatus.undone) { - // Deletion undone, restore the local ad to the main list. - if (item is LocalAd) { - switch (item.adType) { - case 'native': - final updatedAds = List.from(state.nativeAds) - ..insert(0, item as LocalNativeAd); - emit( - state.copyWith( - nativeAds: updatedAds, - lastPendingDeletionId: state.lastPendingDeletionId == id - ? null - : state.lastPendingDeletionId, - snackbarLocalAdTitle: null, - ), - ); - case 'banner': - final updatedAds = List.from(state.bannerAds) - ..insert(0, item as LocalBannerAd); - emit( - state.copyWith( - bannerAds: updatedAds, - lastPendingDeletionId: state.lastPendingDeletionId == id - ? null - : state.lastPendingDeletionId, - snackbarLocalAdTitle: null, - ), - ); - case 'interstitial': - final updatedAds = List.from( - state.interstitialAds, - )..insert(0, item as LocalInterstitialAd); - emit( - state.copyWith( - interstitialAds: updatedAds, - lastPendingDeletionId: state.lastPendingDeletionId == id - ? null - : state.lastPendingDeletionId, - snackbarLocalAdTitle: null, - ), - ); - case 'video': - final updatedAds = List.from(state.videoAds) - ..insert(0, item as LocalVideoAd); - emit( - state.copyWith( - videoAds: updatedAds, - lastPendingDeletionId: state.lastPendingDeletionId == id - ? null - : state.lastPendingDeletionId, - snackbarLocalAdTitle: null, - ), - ); - } - } - } - } -} diff --git a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_event.dart b/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_event.dart deleted file mode 100644 index a4268847..00000000 --- a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_event.dart +++ /dev/null @@ -1,84 +0,0 @@ -part of 'archive_local_ads_bloc.dart'; - -sealed class ArchiveLocalAdsEvent extends Equatable { - const ArchiveLocalAdsEvent(); - - @override - List get props => []; -} - -/// {@template load_archived_local_ads_requested} -/// Event to request loading of archived local ads for a specific type. -/// {@endtemplate} -final class LoadArchivedLocalAdsRequested extends ArchiveLocalAdsEvent { - /// {@macro load_archived_local_ads_requested} - const LoadArchivedLocalAdsRequested({ - required this.adType, - this.startAfterId, - this.limit, - }); - - /// The type of ad to load. - final AdType adType; - - /// Optional ID to start pagination after. - final String? startAfterId; - - /// Optional maximum number of items to return. - final int? limit; - - @override - List get props => [adType, startAfterId, limit]; -} - -/// {@template restore_local_ad_requested} -/// Event to restore an archived local ad. -/// {@endtemplate} -final class RestoreLocalAdRequested extends ArchiveLocalAdsEvent { - /// {@macro restore_local_ad_requested} - const RestoreLocalAdRequested(this.id, this.adType); - - /// The ID of the local ad to restore. - final String id; - - /// The type of the local ad to restore. - final AdType adType; - - @override - List get props => [id, adType]; -} - -/// {@template delete_local_ad_forever_requested} -/// Event to permanently delete an archived local ad. -/// {@endtemplate} -final class DeleteLocalAdForeverRequested extends ArchiveLocalAdsEvent { - /// {@macro delete_local_ad_forever_requested} - const DeleteLocalAdForeverRequested(this.id, this.adType); - - /// The ID of the local ad to delete forever. - final String id; - - /// The type of the local ad to delete forever. - final AdType adType; - - @override - List get props => [id, adType]; -} - -/// {@template undo_delete_local_ad_requested} -/// Event to undo the deletion of a local ad. -/// {@endtemplate} -final class UndoDeleteLocalAdRequested extends ArchiveLocalAdsEvent { - /// {@macro undo_delete_local_ad_requested} - const UndoDeleteLocalAdRequested(); -} - -/// Event to handle updates from the pending deletions service. -final class _DeletionServiceStatusChanged extends ArchiveLocalAdsEvent { - const _DeletionServiceStatusChanged(this.event); - - final DeletionEvent event; - - @override - List get props => [event]; -} diff --git a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_state.dart b/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_state.dart deleted file mode 100644 index 7b5d4393..00000000 --- a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_state.dart +++ /dev/null @@ -1,171 +0,0 @@ -part of 'archive_local_ads_bloc.dart'; - -/// Represents the status of archived local ad operations. -enum ArchiveLocalAdsStatus { - initial, - loading, - success, - failure, -} - -/// Defines the state for the archived local ads feature. -class ArchiveLocalAdsState extends Equatable { - const ArchiveLocalAdsState({ - this.status = ArchiveLocalAdsStatus.initial, - this.nativeAdsStatus = ArchiveLocalAdsStatus.initial, - this.nativeAds = const [], - this.nativeAdsCursor, - this.nativeAdsHasMore = false, - this.bannerAdsStatus = ArchiveLocalAdsStatus.initial, - this.bannerAds = const [], - this.bannerAdsCursor, - this.bannerAdsHasMore = false, - this.interstitialAdsStatus = ArchiveLocalAdsStatus.initial, - this.interstitialAds = const [], - this.interstitialAdsCursor, - this.interstitialAdsHasMore = false, - this.videoAdsStatus = ArchiveLocalAdsStatus.initial, - this.videoAds = const [], - this.videoAdsCursor, - this.videoAdsHasMore = false, - this.exception, - this.lastPendingDeletionId, - this.snackbarLocalAdTitle, - }); - - final ArchiveLocalAdsStatus status; - - /// Status for native ads loading. - final ArchiveLocalAdsStatus nativeAdsStatus; - - /// List of archived native ads. - final List nativeAds; - - /// Cursor for native ad pagination. - final String? nativeAdsCursor; - - /// Indicates if there are more native ads to load. - final bool nativeAdsHasMore; - - /// Status for banner ads loading. - final ArchiveLocalAdsStatus bannerAdsStatus; - - /// List of archived banner ads. - final List bannerAds; - - /// Cursor for banner ad pagination. - final String? bannerAdsCursor; - - /// Indicates if there are more banner ads to load. - final bool bannerAdsHasMore; - - /// Status for interstitial ads loading. - final ArchiveLocalAdsStatus interstitialAdsStatus; - - /// List of archived interstitial ads. - final List interstitialAds; - - /// Cursor for interstitial ad pagination. - final String? interstitialAdsCursor; - - /// Indicates if there are more interstitial ads to load. - final bool interstitialAdsHasMore; - - /// Status for video ads loading. - final ArchiveLocalAdsStatus videoAdsStatus; - - /// List of archived video ads. - final List videoAds; - - /// Cursor for video ad pagination. - final String? videoAdsCursor; - - /// Indicates if there are more video ads to load. - final bool videoAdsHasMore; - - /// The error describing an operation failure, if any. - final HttpException? exception; - - /// The ID of the local ad that was most recently added to pending deletions. - /// Used to trigger the snackbar display. - final String? lastPendingDeletionId; - - /// The title of the local ad for which the snackbar should be displayed. - /// This is set when a deletion is requested and cleared when the snackbar - /// is no longer needed. - final String? snackbarLocalAdTitle; - - ArchiveLocalAdsState copyWith({ - ArchiveLocalAdsStatus? status, - ArchiveLocalAdsStatus? nativeAdsStatus, - List? nativeAds, - String? nativeAdsCursor, - bool? nativeAdsHasMore, - ArchiveLocalAdsStatus? bannerAdsStatus, - List? bannerAds, - String? bannerAdsCursor, - bool? bannerAdsHasMore, - ArchiveLocalAdsStatus? interstitialAdsStatus, - List? interstitialAds, - String? interstitialAdsCursor, - bool? interstitialAdsHasMore, - ArchiveLocalAdsStatus? videoAdsStatus, - List? videoAds, - String? videoAdsCursor, - bool? videoAdsHasMore, - HttpException? exception, - String? lastPendingDeletionId, - String? snackbarLocalAdTitle, - }) { - return ArchiveLocalAdsState( - status: status ?? this.status, - nativeAdsStatus: nativeAdsStatus ?? this.nativeAdsStatus, - nativeAds: nativeAds ?? this.nativeAds, - nativeAdsCursor: nativeAdsCursor ?? this.nativeAdsCursor, - nativeAdsHasMore: nativeAdsHasMore ?? this.nativeAdsHasMore, - bannerAdsStatus: bannerAdsStatus ?? this.bannerAdsStatus, - bannerAds: bannerAds ?? this.bannerAds, - bannerAdsCursor: bannerAdsCursor ?? this.bannerAdsCursor, - bannerAdsHasMore: bannerAdsHasMore ?? this.bannerAdsHasMore, - interstitialAdsStatus: - interstitialAdsStatus ?? this.interstitialAdsStatus, - interstitialAds: interstitialAds ?? this.interstitialAds, - interstitialAdsCursor: - interstitialAdsCursor ?? this.interstitialAdsCursor, - interstitialAdsHasMore: - interstitialAdsHasMore ?? this.interstitialAdsHasMore, - videoAdsStatus: videoAdsStatus ?? this.videoAdsStatus, - videoAds: videoAds ?? this.videoAds, - videoAdsCursor: videoAdsCursor ?? this.videoAdsCursor, - videoAdsHasMore: videoAdsHasMore ?? this.videoAdsHasMore, - exception: exception, - lastPendingDeletionId: - lastPendingDeletionId ?? this.lastPendingDeletionId, - snackbarLocalAdTitle: snackbarLocalAdTitle, - ); - } - - @override - List get props => [ - status, - nativeAdsStatus, - nativeAds, - nativeAdsCursor, - nativeAdsHasMore, - bannerAdsStatus, - bannerAds, - bannerAdsCursor, - bannerAdsHasMore, - interstitialAdsStatus, - interstitialAds, - interstitialAdsCursor, - interstitialAdsHasMore, - videoAdsStatus, - videoAds, - videoAdsCursor, - videoAdsHasMore, - exception, - lastPendingDeletionId, - snackbarLocalAdTitle, - ]; -} diff --git a/lib/local_ads_management/view/archived_local_ads_page.dart b/lib/local_ads_management/view/archived_local_ads_page.dart deleted file mode 100644 index c5a04956..00000000 --- a/lib/local_ads_management/view/archived_local_ads_page.dart +++ /dev/null @@ -1,364 +0,0 @@ -import 'package:core/core.dart'; -import 'package:data_repository/data_repository.dart'; -import 'package:data_table_2/data_table_2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_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/local_ads_management/bloc/archive_local_ads/archive_local_ads_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; -import 'package:intl/intl.dart'; -import 'package:ui_kit/ui_kit.dart'; - -/// {@template archived_local_ads_page} -/// A page for viewing and managing archived local advertisements. -/// {@endtemplate} -class ArchivedLocalAdsPage extends StatelessWidget { - /// {@macro archived_local_ads_page} - const ArchivedLocalAdsPage({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - ArchiveLocalAdsBloc( - localAdsRepository: context.read>(), - pendingDeletionsService: context.read(), - ) - ..add(const LoadArchivedLocalAdsRequested(adType: AdType.native)) - ..add(const LoadArchivedLocalAdsRequested(adType: AdType.banner)) - ..add( - const LoadArchivedLocalAdsRequested(adType: AdType.interstitial), - ) - ..add(const LoadArchivedLocalAdsRequested(adType: AdType.video)), - child: const _ArchivedLocalAdsView(), - ); - } -} - -class _ArchivedLocalAdsView extends StatefulWidget { - const _ArchivedLocalAdsView(); - - @override - State<_ArchivedLocalAdsView> createState() => _ArchivedLocalAdsViewState(); -} - -class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> - with SingleTickerProviderStateMixin { - late TabController _tabController; - - // Statically define the tab order to ensure consistency. - final List _tabs = [ - AdType.native, - AdType.banner, - AdType.interstitial, - AdType.video, - ]; - - @override - void initState() { - super.initState(); - _tabController = TabController( - length: _tabs.length, - vsync: this, - ); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - // Read the PendingDeletionsService instance directly. - // This is safe because PendingDeletionsService is a singleton and does not - // depend on the widget's BuildContext for its operations, preventing - // "deactivated widget's ancestor" errors when the SnackBar is dismissed - // after the widget has been unmounted. - final pendingDeletionsService = context.read(); - - return Scaffold( - appBar: AppBar( - title: Text(l10n.archivedLocalAdsTitle), - bottom: TabBar( - controller: _tabController, - tabAlignment: TabAlignment.start, - isScrollable: true, - tabs: _tabs.map((type) => Tab(text: type.l10n(context))).toList(), - ), - ), - body: BlocListener( - listenWhen: (previous, current) => - previous.lastPendingDeletionId != current.lastPendingDeletionId || - previous.snackbarLocalAdTitle != current.snackbarLocalAdTitle, - listener: (context, state) { - if (state.snackbarLocalAdTitle != null) { - final adId = state.lastPendingDeletionId!; - final truncatedTitle = state.snackbarLocalAdTitle!.truncate(30); - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text( - l10n.localAdDeleted(truncatedTitle), - ), - action: SnackBarAction( - label: l10n.undo, - onPressed: () { - // Directly call undoDeletion on the service. - // This avoids using context.read on a potentially deactivated - // widget, which caused the "deactivated widget's ancestor" error. - pendingDeletionsService.undoDeletion(adId); - }, - ), - ), - ); - } - }, - child: TabBarView( - controller: _tabController, - children: _tabs - .map((type) => _ArchivedLocalAdsDataTable(adType: type)) - .toList(), - ), - ), - ); - } -} - -class _ArchivedLocalAdsDataTable extends StatelessWidget { - const _ArchivedLocalAdsDataTable({required this.adType}); - - final AdType adType; - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - return BlocBuilder( - builder: (context, state) { - ArchiveLocalAdsStatus status; - List ads; - String? cursor; - bool hasMore; - - switch (adType) { - case AdType.native: - status = state.nativeAdsStatus; - ads = state.nativeAds; - cursor = state.nativeAdsCursor; - hasMore = state.nativeAdsHasMore; - case AdType.banner: - status = state.bannerAdsStatus; - ads = state.bannerAds; - cursor = state.bannerAdsCursor; - hasMore = state.bannerAdsHasMore; - case AdType.interstitial: - status = state.interstitialAdsStatus; - ads = state.interstitialAds; - cursor = state.interstitialAdsCursor; - hasMore = state.interstitialAdsHasMore; - case AdType.video: - status = state.videoAdsStatus; - ads = state.videoAds; - cursor = state.videoAdsCursor; - hasMore = state.videoAdsHasMore; - } - - if (status == ArchiveLocalAdsStatus.loading && ads.isEmpty) { - return LoadingStateWidget( - icon: Icons.ads_click, - headline: l10n.loadingArchivedLocalAds, - subheadline: l10n.pleaseWait, - ); - } - - if (status == ArchiveLocalAdsStatus.failure) { - return FailureStateWidget( - exception: state.exception!, - onRetry: () => context.read().add( - LoadArchivedLocalAdsRequested( - adType: adType, - limit: kDefaultRowsPerPage, - ), - ), - ); - } - - if (ads.isEmpty) { - return Center(child: Text(l10n.noArchivedLocalAdsFound)); - } - - return Column( - children: [ - if (status == ArchiveLocalAdsStatus.loading && ads.isNotEmpty) - const LinearProgressIndicator(), - Expanded( - child: PaginatedDataTable2( - columns: [ - DataColumn2( - label: Text(l10n.adTitle), - size: ColumnSize.L, - ), - DataColumn2( - label: Text(l10n.lastUpdated), - size: ColumnSize.M, - ), - DataColumn2( - label: Text(l10n.status), - size: ColumnSize.S, - ), - DataColumn2( - label: Text(l10n.actions), - size: ColumnSize.S, - fixedWidth: 180, - ), - ], - source: _ArchivedLocalAdsDataSource( - context: context, - ads: ads, - hasMore: hasMore, - l10n: l10n, - adType: adType, - ), - rowsPerPage: kDefaultRowsPerPage, - availableRowsPerPage: const [kDefaultRowsPerPage], - onPageChanged: (pageIndex) { - final newOffset = pageIndex * kDefaultRowsPerPage; - if (newOffset >= ads.length && - hasMore && - status != ArchiveLocalAdsStatus.loading) { - context.read().add( - LoadArchivedLocalAdsRequested( - adType: adType, - startAfterId: cursor, - limit: kDefaultRowsPerPage, - ), - ); - } - }, - empty: Center(child: Text(l10n.noArchivedLocalAdsFound)), - showCheckboxColumn: false, - showFirstLastButtons: true, - fit: FlexFit.tight, - headingRowHeight: 56, - dataRowHeight: 56, - columnSpacing: AppSpacing.md, - horizontalMargin: AppSpacing.md, - ), - ), - ], - ); - }, - ); - } -} - -class _ArchivedLocalAdsDataSource extends DataTableSource { - _ArchivedLocalAdsDataSource({ - required this.context, - required this.ads, - required this.hasMore, - required this.l10n, - required this.adType, - }); - - final BuildContext context; - final List ads; - final bool hasMore; - final AppLocalizations l10n; - final AdType adType; - - @override - DataRow? getRow(int index) { - if (index >= ads.length) { - return null; - } - final ad = ads[index]; - String title; - DateTime updatedAt; - ContentStatus status; - - // Determine title, updatedAt, and status based on ad type - switch (ad.adType) { - case 'native': - final nativeAd = ad as LocalNativeAd; - title = nativeAd.title; - updatedAt = nativeAd.updatedAt; - status = nativeAd.status; - case 'banner': - final bannerAd = ad as LocalBannerAd; - title = bannerAd.imageUrl; - updatedAt = bannerAd.updatedAt; - status = bannerAd.status; - case 'interstitial': - final interstitialAd = ad as LocalInterstitialAd; - title = interstitialAd.imageUrl; - updatedAt = interstitialAd.updatedAt; - status = interstitialAd.status; - case 'video': - final videoAd = ad as LocalVideoAd; - title = videoAd.videoUrl; - updatedAt = videoAd.updatedAt; - status = videoAd.status; - default: - title = 'Unknown Ad Type'; - updatedAt = DateTime.now(); - status = ContentStatus.active; - } - - return DataRow2( - cells: [ - DataCell( - Text( - title.truncate(50), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - DataCell( - Text( - DateFormat('dd-MM-yyyy').format(updatedAt.toLocal()), - ), - ), - DataCell(Text(status.l10n(context))), - DataCell( - Row( - children: [ - IconButton( - icon: const Icon(Icons.restore), - tooltip: l10n.restore, - onPressed: () { - context.read().add( - RestoreLocalAdRequested(ad.id, adType), - ); - }, - ), - IconButton( - icon: const Icon(Icons.delete_forever), - tooltip: l10n.deleteForever, - onPressed: () { - context.read().add( - DeleteLocalAdForeverRequested(ad.id, adType), - ); - }, - ), - ], - ), - ), - ], - ); - } - - @override - bool get isRowCountApproximate => hasMore; - - @override - int get rowCount => ads.length; - - @override - int get selectedRowCount => 0; -} From b029f23184ab80df2c3876cd21fb99f1db51002e Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 16:32:55 +0100 Subject: [PATCH 22/66] feat(local_ads_management): auto-refresh local ads list after archive or restore - Add LoadLocalAdsRequested event after successfully archiving or restoring local ads - This ensures the UI is updated immediately to reflect the changed status of the ads - Helps users see the results of their actions more quickly and improves overall user experience --- .../bloc/local_ads_management_bloc.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/local_ads_management/bloc/local_ads_management_bloc.dart b/lib/local_ads_management/bloc/local_ads_management_bloc.dart index c7a7e815..1b1e7968 100644 --- a/lib/local_ads_management/bloc/local_ads_management_bloc.dart +++ b/lib/local_ads_management/bloc/local_ads_management_bloc.dart @@ -327,6 +327,14 @@ class LocalAdsManagementBloc exception: null, ), ); // Clear exception on success + // Explicitly trigger a refresh after archiving + add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: buildLocalAdsFilterMap(_filterLocalAdsBloc.state), + ), + ); } on HttpException catch (e) { emit(state.copyWith(exception: e)); } catch (e) { @@ -365,6 +373,14 @@ class LocalAdsManagementBloc exception: null, ), ); // Clear exception on success + // Explicitly trigger a refresh after restoring + add( + LoadLocalAdsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: buildLocalAdsFilterMap(_filterLocalAdsBloc.state), + ), + ); } on HttpException catch (e) { emit(state.copyWith(exception: e)); } catch (e) { From 5fd5866dac3ef8d64d853527239884a8fb894407 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 16:33:05 +0100 Subject: [PATCH 23/66] refactor(local_ads_management): replace action buttons with widget - Remove direct implementation of action buttons in banner ads page - Replace with LocalAdActionButtons widget import and usage - Simplify code and improve maintainability by using a dedicated widget --- .../view/banner_ads_page.dart | 63 +------------------ 1 file changed, 2 insertions(+), 61 deletions(-) diff --git a/lib/local_ads_management/view/banner_ads_page.dart b/lib/local_ads_management/view/banner_ads_page.dart index 6e11a0f1..96300226 100644 --- a/lib/local_ads_management/view/banner_ads_page.dart +++ b/lib/local_ads_management/view/banner_ads_page.dart @@ -1,15 +1,13 @@ import 'package:core/core.dart'; import 'package:data_table_2/data_table_2.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; // Import the new action buttons widget import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; -import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -227,64 +225,7 @@ class _BannerAdsDataSource extends DataTableSource { ), ), DataCell( - Row( - children: [ - // Primary action: Copy ID button - IconButton( - icon: const Icon(Icons.copy), - tooltip: l10n.copyId, - onPressed: () { - Clipboard.setData(ClipboardData(text: ad.id)); - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text(l10n.idCopiedToClipboard(ad.id)), - ), - ); - }, - ), - // Secondary actions: Edit and Archive via PopupMenuButton - PopupMenuButton( - icon: const Icon(Icons.more_vert), - tooltip: l10n.moreActions, - onSelected: (value) { - if (value == 'edit') { - context.goNamed( - Routes.editLocalBannerAdName, - pathParameters: {'id': ad.id}, - ); - } else if (value == 'archive') { - context.read().add( - ArchiveLocalAdRequested(ad.id), - ); - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: 'edit', - child: Row( - children: [ - const Icon(Icons.edit), - const SizedBox(width: AppSpacing.sm), - Text(l10n.editLocalAds), - ], - ), - ), - PopupMenuItem( - value: 'archive', - child: Row( - children: [ - const Icon(Icons.archive), - const SizedBox(width: AppSpacing.sm), - Text(l10n.archive), - ], - ), - ), - ], - ), - ], - ), + LocalAdActionButtons(item: ad, l10n: l10n), // Use the new widget ), ], ); From b907ca1bba43f046f83ee07aa27fb13b571d8a50 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 16:33:18 +0100 Subject: [PATCH 24/66] refactor(local_ads_management): extract ad action buttons to widget - Replace inline ad action buttons with LocalAdActionButtons widget - Remove unnecessary imports - Add comments for clarity - Minor code formatting and cleanup --- .../view/interstitial_ads_page.dart | 81 +++---------------- 1 file changed, 9 insertions(+), 72 deletions(-) diff --git a/lib/local_ads_management/view/interstitial_ads_page.dart b/lib/local_ads_management/view/interstitial_ads_page.dart index e5f1aa01..6bb284d6 100644 --- a/lib/local_ads_management/view/interstitial_ads_page.dart +++ b/lib/local_ads_management/view/interstitial_ads_page.dart @@ -1,15 +1,13 @@ import 'package:core/core.dart'; import 'package:data_table_2/data_table_2.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; // Import the new action buttons widget import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; -import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -34,7 +32,7 @@ class _InterstitialAdsPageState extends State { limit: kDefaultRowsPerPage, filter: context.read().buildLocalAdsFilterMap( context.read().state.copyWith( - selectedAdType: AdType.interstitial, + selectedAdType: AdType.interstitial, // Ensure correct ad type is set for filter ), ), ), @@ -56,7 +54,7 @@ class _InterstitialAdsPageState extends State { padding: const EdgeInsets.only(top: AppSpacing.sm), child: BlocBuilder( builder: (context, state) { - final filterLocalAdsState = context.watch().state; + final filterLocalAdsState = context.watch().state; // Watch filter state final filtersActive = _areFiltersActive(filterLocalAdsState); if (state.interstitialAdsStatus == LocalAdsManagementStatus.loading && @@ -88,7 +86,7 @@ class _InterstitialAdsPageState extends State { } if (state.interstitialAds.isEmpty) { - if (filtersActive) { + if (filtersActive) { // Conditionally show reset button if filters are active return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -123,8 +121,7 @@ class _InterstitialAdsPageState extends State { return Column( children: [ - if (state.interstitialAdsStatus == - LocalAdsManagementStatus.loading && + if (state.interstitialAdsStatus == LocalAdsManagementStatus.loading && state.interstitialAds.isNotEmpty) const LinearProgressIndicator(), Expanded( @@ -164,12 +161,9 @@ class _InterstitialAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context - .read() - .state - .copyWith( - selectedAdType: AdType.interstitial, - ), + context.read().state.copyWith( + selectedAdType: AdType.interstitial, + ), ), ), ); @@ -228,64 +222,7 @@ class _InterstitialAdsDataSource extends DataTableSource { ), ), DataCell( - Row( - children: [ - // Primary action: Copy ID button - IconButton( - icon: const Icon(Icons.copy), - tooltip: l10n.copyId, - onPressed: () { - Clipboard.setData(ClipboardData(text: ad.id)); - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text(l10n.idCopiedToClipboard(ad.id)), - ), - ); - }, - ), - // Secondary actions: Edit and Archive via PopupMenuButton - PopupMenuButton( - icon: const Icon(Icons.more_vert), - tooltip: l10n.moreActions, - onSelected: (value) { - if (value == 'edit') { - context.goNamed( - Routes.editLocalInterstitialAdName, - pathParameters: {'id': ad.id}, - ); - } else if (value == 'archive') { - context.read().add( - ArchiveLocalAdRequested(ad.id), - ); - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: 'edit', - child: Row( - children: [ - const Icon(Icons.edit), - const SizedBox(width: AppSpacing.sm), - Text(l10n.editLocalAds), - ], - ), - ), - PopupMenuItem( - value: 'archive', - child: Row( - children: [ - const Icon(Icons.archive), - const SizedBox(width: AppSpacing.sm), - Text(l10n.archive), - ], - ), - ), - ], - ), - ], - ), + LocalAdActionButtons(item: ad, l10n: l10n), // Use the new widget ), ], ); From 49c4011b467a993b6d2f041068cabe390f49e26f Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 16:33:35 +0100 Subject: [PATCH 25/66] refactor(local_ads_management): improve native ads page functionality - Add local_ad_action_buttons widget for ad item actions - Implement filter logic specific to native ads - Display no results message and reset filters button when no ads found - Remove unused imports and update routes --- .../view/native_ads_page.dart | 110 ++++++++---------- 1 file changed, 46 insertions(+), 64 deletions(-) diff --git a/lib/local_ads_management/view/native_ads_page.dart b/lib/local_ads_management/view/native_ads_page.dart index d488c79d..dbf9bf9a 100644 --- a/lib/local_ads_management/view/native_ads_page.dart +++ b/lib/local_ads_management/view/native_ads_page.dart @@ -1,15 +1,13 @@ import 'package:core/core.dart'; import 'package:data_table_2/data_table_2.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; // Import the new action buttons widget import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; -import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -33,7 +31,9 @@ class _NativeAdsPageState extends State { LoadLocalAdsRequested( limit: kDefaultRowsPerPage, filter: context.read().buildLocalAdsFilterMap( - context.read().state, + context.read().state.copyWith( + selectedAdType: AdType.native, // Ensure correct ad type is set for filter + ), ), ), ); @@ -46,6 +46,11 @@ class _NativeAdsPageState extends State { padding: const EdgeInsets.only(top: AppSpacing.sm), child: BlocBuilder( builder: (context, state) { + final filterLocalAdsState = context.watch().state; // Watch filter state + final filtersActive = filterLocalAdsState.searchQuery.isNotEmpty || + filterLocalAdsState.selectedStatus != ContentStatus.active || + filterLocalAdsState.selectedAdType != AdType.native; // Check filters for native ads + if (state.nativeAdsStatus == LocalAdsManagementStatus.loading && state.nativeAds.isEmpty) { return LoadingStateWidget( @@ -65,7 +70,9 @@ class _NativeAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context.read().state, + context.read().state.copyWith( + selectedAdType: AdType.native, + ), ), ), ), @@ -73,6 +80,36 @@ class _NativeAdsPageState extends State { } if (state.nativeAds.isEmpty) { + if (filtersActive) { // Conditionally show reset button if filters are active + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.noResultsWithCurrentFilters, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.lg), + ElevatedButton( + onPressed: () { + context.read().add( + const FilterLocalAdsReset(), + ); + context.read().add( + const FilterLocalAdsApplied( + searchQuery: '', + selectedStatus: ContentStatus.active, + selectedAdType: AdType.native, + ), + ); + }, + child: Text(l10n.resetFiltersButtonText), + ), + ], + ), + ); + } return Center(child: Text(l10n.noNativeAdsFound)); } @@ -118,7 +155,9 @@ class _NativeAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context.read().state, + context.read().state.copyWith( + selectedAdType: AdType.native, + ), ), ), ); @@ -177,64 +216,7 @@ class _NativeAdsDataSource extends DataTableSource { ), ), DataCell( - Row( - children: [ - // Primary action: Copy ID button - IconButton( - icon: const Icon(Icons.copy), - tooltip: l10n.copyId, - onPressed: () { - Clipboard.setData(ClipboardData(text: ad.id)); - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text(l10n.idCopiedToClipboard(ad.id)), - ), - ); - }, - ), - // Secondary actions: Edit and Archive via PopupMenuButton - PopupMenuButton( - icon: const Icon(Icons.more_vert), - tooltip: l10n.moreActions, - onSelected: (value) { - if (value == 'edit') { - context.goNamed( - Routes.editLocalNativeAdName, - pathParameters: {'id': ad.id}, - ); - } else if (value == 'archive') { - context.read().add( - ArchiveLocalAdRequested(ad.id), - ); - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: 'edit', - child: Row( - children: [ - const Icon(Icons.edit), - const SizedBox(width: AppSpacing.sm), - Text(l10n.editLocalAds), - ], - ), - ), - PopupMenuItem( - value: 'archive', - child: Row( - children: [ - const Icon(Icons.archive), - const SizedBox(width: AppSpacing.sm), - Text(l10n.archive), - ], - ), - ), - ], - ), - ], - ), + LocalAdActionButtons(item: ad, l10n: l10n), // Use the new widget ), ], ); From 08584d455a01089e9d8b247bc86a1b7a9829d447 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 16:33:44 +0100 Subject: [PATCH 26/66] refactor(local_ads_management): implement LocalAdActionButtons widget - Replace ad action buttons with LocalAdActionButtons widget - Remove unused imports - Simplify code structure - Improve readability and maintainability --- .../view/video_ads_page.dart | 82 +++---------------- 1 file changed, 10 insertions(+), 72 deletions(-) diff --git a/lib/local_ads_management/view/video_ads_page.dart b/lib/local_ads_management/view/video_ads_page.dart index 98d9d9e6..c8b694d3 100644 --- a/lib/local_ads_management/view/video_ads_page.dart +++ b/lib/local_ads_management/view/video_ads_page.dart @@ -1,15 +1,13 @@ import 'package:core/core.dart'; import 'package:data_table_2/data_table_2.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_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/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; // Import the new action buttons widget import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; -import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -34,7 +32,7 @@ class _VideoAdsPageState extends State { limit: kDefaultRowsPerPage, filter: context.read().buildLocalAdsFilterMap( context.read().state.copyWith( - selectedAdType: AdType.video, + selectedAdType: AdType.video, // Ensure correct ad type is set for filter ), ), ), @@ -56,7 +54,7 @@ class _VideoAdsPageState extends State { padding: const EdgeInsets.only(top: AppSpacing.sm), child: BlocBuilder( builder: (context, state) { - final filterLocalAdsState = context.watch().state; + final filterLocalAdsState = context.watch().state; // Watch filter state final filtersActive = _areFiltersActive(filterLocalAdsState); if (state.videoAdsStatus == LocalAdsManagementStatus.loading && @@ -88,7 +86,7 @@ class _VideoAdsPageState extends State { } if (state.videoAds.isEmpty) { - if (filtersActive) { + if (filtersActive) { // Conditionally show reset button if filters are active return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -130,7 +128,7 @@ class _VideoAdsPageState extends State { child: PaginatedDataTable2( columns: [ DataColumn2( - label: Text(l10n.adVideoUrl), + label: Text(l10n.adVideoUrl), // Changed label to adVideoUrl size: ColumnSize.L, ), DataColumn2( @@ -163,12 +161,9 @@ class _VideoAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context - .read() - .state - .copyWith( - selectedAdType: AdType.video, - ), + context.read().state.copyWith( + selectedAdType: AdType.video, + ), ), ), ); @@ -216,7 +211,7 @@ class _VideoAdsDataSource extends DataTableSource { cells: [ DataCell( Text( - ad.videoUrl.truncate(50), + ad.videoUrl.truncate(50), // Changed to videoUrl maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -227,64 +222,7 @@ class _VideoAdsDataSource extends DataTableSource { ), ), DataCell( - Row( - children: [ - // Primary action: Copy ID button - IconButton( - icon: const Icon(Icons.copy), - tooltip: l10n.copyId, - onPressed: () { - Clipboard.setData(ClipboardData(text: ad.id)); - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text(l10n.idCopiedToClipboard(ad.id)), - ), - ); - }, - ), - // Secondary actions: Edit and Archive via PopupMenuButton - PopupMenuButton( - icon: const Icon(Icons.more_vert), - tooltip: l10n.moreActions, - onSelected: (value) { - if (value == 'edit') { - context.goNamed( - Routes.editLocalVideoAdName, - pathParameters: {'id': ad.id}, - ); - } else if (value == 'archive') { - context.read().add( - ArchiveLocalAdRequested(ad.id), - ); - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: 'edit', - child: Row( - children: [ - const Icon(Icons.edit), - const SizedBox(width: AppSpacing.sm), - Text(l10n.editLocalAds), - ], - ), - ), - PopupMenuItem( - value: 'archive', - child: Row( - children: [ - const Icon(Icons.archive), - const SizedBox(width: AppSpacing.sm), - Text(l10n.archive), - ], - ), - ), - ], - ), - ], - ), + LocalAdActionButtons(item: ad, l10n: l10n), // Use the new widget ), ], ); From aaeac03f84e3f7d8511407ff2b1cd6ad19a59ebf Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 16:33:57 +0100 Subject: [PATCH 27/66] feat(local_ads_management): add LocalAdActionButtons widget - Implement LocalAdActionButtons widget for displaying action buttons related to local ad items. - The widget supports different local ad types (Native, Banner, Interstitial, Video) and their statuses. - Features include editing ads and performing status transitions (publish, archive, restore, delete). - Utilizes GoRouter for navigation to edit screens. - Follows the design pattern of ContentActionButtons for content management items. --- .../widgets/local_ad_action_buttons.dart | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 lib/local_ads_management/widgets/local_ad_action_buttons.dart diff --git a/lib/local_ads_management/widgets/local_ad_action_buttons.dart b/lib/local_ads_management/widgets/local_ad_action_buttons.dart new file mode 100644 index 00000000..d7e45e9b --- /dev/null +++ b/lib/local_ads_management/widgets/local_ad_action_buttons.dart @@ -0,0 +1,189 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/widgets/content_action_buttons.dart' show ContentActionButtons; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:go_router/go_router.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template local_ad_action_buttons} +/// A widget that displays action buttons for local ad items +/// (Native, Banner, Interstitial, Video) based on their [ContentStatus]. +/// +/// It shows a maximum of two primary icons, with additional actions +/// accessible via a dropdown menu, mirroring the functionality of +/// [ContentActionButtons] for content management items. +/// {@endtemplate} +class LocalAdActionButtons extends StatelessWidget { + /// {@macro local_ad_action_buttons} + const LocalAdActionButtons({ + required this.item, + required this.l10n, + super.key, + }); + + /// The local ad item for which to display actions. + final LocalAd item; + + /// The localized strings for the application. + final AppLocalizations l10n; + + @override + Widget build(BuildContext context) { + final visibleActions = []; + final overflowMenuItems = >[]; + + // Determine item ID and status + final itemId = item.id; + final status = switch (item) { + final LocalNativeAd ad => ad.status, + final LocalBannerAd ad => ad.status, + final LocalInterstitialAd ad => ad.status, + final LocalVideoAd ad => ad.status, + // Wildcard pattern to ensure exhaustive matching for LocalAd subtypes. + _ => throw StateError('Unknown LocalAd type: ${item.runtimeType}'), + }; + + // Action 1: Edit (always visible as the first action) + visibleActions.add( + IconButton( + visualDensity: VisualDensity.compact, + iconSize: 20, + icon: const Icon(Icons.edit), + tooltip: l10n.edit, + onPressed: () { + String routeName; + switch (item.adType) { + case 'native': + routeName = Routes.editLocalNativeAdName; + case 'banner': + routeName = Routes.editLocalBannerAdName; + case 'interstitial': + routeName = Routes.editLocalInterstitialAdName; + case 'video': + routeName = Routes.editLocalVideoAdName; + default: + return; + } + context.goNamed( + routeName, + pathParameters: {'id': itemId}, + ); + }, + ), + ); + + // Determine contextual action and add to overflow + switch (status) { + case ContentStatus.draft: + // Local ads are not expected to have a 'draft' status in this context, + // but including for completeness if the model changes. + overflowMenuItems.add( + PopupMenuItem( + value: 'publish', + child: Row( + children: [ + const Icon(Icons.publish), + const SizedBox(width: AppSpacing.sm), + Text(l10n.publish), + ], + ), + ), + ); + overflowMenuItems.add( + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + const Icon(Icons.delete_forever), + const SizedBox(width: AppSpacing.sm), + Text(l10n.deleteForever), + ], + ), + ), + ); + case ContentStatus.active: + overflowMenuItems.add( + PopupMenuItem( + value: 'archive', + child: Row( + children: [ + const Icon(Icons.archive), + const SizedBox(width: AppSpacing.sm), + Text(l10n.archive), + ], + ), + ), + ); + case ContentStatus.archived: + overflowMenuItems.add( + PopupMenuItem( + value: 'restore', + child: Row( + children: [ + const Icon(Icons.unarchive), + const SizedBox(width: AppSpacing.sm), + Text(l10n.restore), + ], + ), + ), + ); + overflowMenuItems.add( + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + const Icon(Icons.delete_forever), + const SizedBox(width: AppSpacing.sm), + Text(l10n.deleteForever), + ], + ), + ), + ); + } + + // The ellipsis button is always shown if there are any overflow actions. + // Given the current logic, there will always be at least one overflow item + // (archive/restore/delete). + visibleActions.add( + SizedBox( + width: 32, + child: PopupMenuButton( + iconSize: 20, + icon: const Icon(Icons.more_vert), + tooltip: l10n.moreActions, + onSelected: (value) { + switch (value) { + case 'publish': + // Local ads are not expected to be 'published' from draft, + // but including for completeness. + context.read().add( + ArchiveLocalAdRequested(itemId), // Treat publish as archive for now + ); + case 'archive': + context.read().add( + ArchiveLocalAdRequested(itemId), + ); + case 'restore': + context.read().add( + RestoreLocalAdRequested(itemId), + ); + case 'delete': + context.read().add( + DeleteLocalAdForeverRequested(itemId), + ); + } + }, + itemBuilder: (BuildContext context) => overflowMenuItems, + ), + ), + ); + + return Row( + mainAxisSize: MainAxisSize.min, + children: visibleActions, + ); + } +} From bce458ebdb98439d7b685a2aeda021fbf1978e3b Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 16:34:09 +0100 Subject: [PATCH 28/66] refactor(local_ads_management): remove unused constructor parameter - Removed `filterLocalAdsBloc` parameter from `LocalAdsFilterDialogBloc` constructor - Deleted `_filterLocalAdsBloc` class member variable - Updated constructor to use simple syntax --- .../bloc/local_ads_filter_dialog_bloc.dart | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart b/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart index db2e7595..0b70d722 100644 --- a/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart +++ b/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart @@ -23,10 +23,7 @@ EventTransformer debounce(Duration duration) { class LocalAdsFilterDialogBloc extends Bloc { /// {@macro local_ads_filter_dialog_bloc} - LocalAdsFilterDialogBloc({ - required FilterLocalAdsBloc filterLocalAdsBloc, - }) : _filterLocalAdsBloc = filterLocalAdsBloc, - super(const LocalAdsFilterDialogState()) { + LocalAdsFilterDialogBloc() : super(const LocalAdsFilterDialogState()) { on(_onLocalAdsFilterDialogInitialized); on( _onLocalAdsFilterDialogSearchQueryChanged, @@ -37,8 +34,6 @@ class LocalAdsFilterDialogBloc on(_onLocalAdsFilterDialogReset); } - final FilterLocalAdsBloc _filterLocalAdsBloc; - /// Initializes the filter dialog's state from the current filter BLoC. void _onLocalAdsFilterDialogInitialized( LocalAdsFilterDialogInitialized event, From 52c98ec936e8af00031f7dba02f8ae71535530bc Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 16:34:22 +0100 Subject: [PATCH 29/66] refactor(router): remove explicit constructor parameters - Remove explicit constructor parameters for LocalAdsFilterDialogBloc - Keep using default constructor and chaining init method call --- lib/router/router.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/router/router.dart b/lib/router/router.dart index 637eb7b2..5ca41750 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -318,9 +318,7 @@ GoRouter createRouter({ fullscreenDialog: true, child: BlocProvider( create: (providerContext) => - LocalAdsFilterDialogBloc( - filterLocalAdsBloc: filterLocalAdsBloc, - )..add( + LocalAdsFilterDialogBloc()..add( LocalAdsFilterDialogInitialized( filterLocalAdsState: filterLocalAdsBloc.state, From 2f78b4fc41431403e1aceeefc785f11a4f2676bb Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 16:34:46 +0100 Subject: [PATCH 30/66] feat(local_ads): implement LocalAdsManagementBloc - Add import for LocalAdsManagementBloc - Implement LocalAdsManagementBloc in App widget - Provide necessary dependencies for LocalAdsManagementBloc --- lib/app/view/app.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 8506a2ee..e3c376e5 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -17,6 +17,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/topics_filter/topics_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; // Added import for LocalAdsManagementBloc import 'package:flutter_news_app_web_dashboard_full_source_code/overview/bloc/overview_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/router.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; @@ -146,6 +147,13 @@ class App extends StatelessWidget { pendingDeletionsService: context.read(), ), ), + BlocProvider( + create: (context) => LocalAdsManagementBloc( // Added LocalAdsManagementBloc + localAdsRepository: context.read>(), + filterLocalAdsBloc: context.read(), + pendingDeletionsService: context.read(), + ), + ), BlocProvider( create: (context) => OverviewBloc( dashboardSummaryRepository: context From 0a5e3f1be3613864dc705d3d694959ca1a646277 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 16:36:05 +0100 Subject: [PATCH 31/66] style: format --- lib/app/view/app.dart | 4 +-- .../bloc/local_ads_management_bloc.dart | 28 +++++++++---------- .../view/banner_ads_page.dart | 4 +-- .../view/interstitial_ads_page.dart | 23 +++++++++------ .../view/local_ads_management_page.dart | 2 +- .../view/native_ads_page.dart | 25 ++++++++++------- .../view/video_ads_page.dart | 26 ++++++++++------- .../widgets/local_ad_action_buttons.dart | 5 ++-- 8 files changed, 67 insertions(+), 50 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index e3c376e5..2fa742c2 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -17,7 +17,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/topics_filter/topics_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; // Added import for LocalAdsManagementBloc +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/overview/bloc/overview_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/router.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; @@ -148,7 +148,7 @@ class App extends StatelessWidget { ), ), BlocProvider( - create: (context) => LocalAdsManagementBloc( // Added LocalAdsManagementBloc + create: (context) => LocalAdsManagementBloc( localAdsRepository: context.read>(), filterLocalAdsBloc: context.read(), pendingDeletionsService: context.read(), diff --git a/lib/local_ads_management/bloc/local_ads_management_bloc.dart b/lib/local_ads_management/bloc/local_ads_management_bloc.dart index 1b1e7968..eadb8208 100644 --- a/lib/local_ads_management/bloc/local_ads_management_bloc.dart +++ b/lib/local_ads_management/bloc/local_ads_management_bloc.dart @@ -1,13 +1,13 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; -import 'package:collection/collection.dart'; // Import for firstWhereOrNull +import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.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/extensions/string_truncate.dart'; // Import for truncate +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -185,7 +185,7 @@ class LocalAdsManagementBloc ], nativeAdsCursor: paginatedAds.cursor, nativeAdsHasMore: paginatedAds.hasMore, - exception: null, // Clear any previous exception + exception: null, ), ); case AdType.banner: @@ -201,7 +201,7 @@ class LocalAdsManagementBloc ], bannerAdsCursor: paginatedAds.cursor, bannerAdsHasMore: paginatedAds.hasMore, - exception: null, // Clear any previous exception + exception: null, ), ); case AdType.interstitial: @@ -217,7 +217,7 @@ class LocalAdsManagementBloc ], interstitialAdsCursor: paginatedAds.cursor, interstitialAdsHasMore: paginatedAds.hasMore, - exception: null, // Clear any previous exception + exception: null, ), ); case AdType.video: @@ -231,7 +231,7 @@ class LocalAdsManagementBloc ], videoAdsCursor: paginatedAds.cursor, videoAdsHasMore: paginatedAds.hasMore, - exception: null, // Clear any previous exception + exception: null, ), ); } @@ -326,7 +326,7 @@ class LocalAdsManagementBloc snackbarMessage: 'Ad archived successfully.', exception: null, ), - ); // Clear exception on success + ); // Explicitly trigger a refresh after archiving add( LoadLocalAdsRequested( @@ -372,7 +372,7 @@ class LocalAdsManagementBloc snackbarMessage: 'Ad restored successfully.', exception: null, ), - ); // Clear exception on success + ); // Explicitly trigger a refresh after restoring add( LoadLocalAdsRequested( @@ -442,7 +442,7 @@ class LocalAdsManagementBloc snackbarMessage: 'Ad "${adToDelete.id.truncate(30)}" deleted.', exception: null, ), - ); // Clear exception on success + ); _pendingDeletionsService.requestDeletion( item: adToDelete, @@ -473,7 +473,7 @@ class LocalAdsManagementBloc emit( state.copyWith( snackbarMessage: null, - exception: null, // Clear any previous exception + exception: null, ), ); case DeletionStatus.undone: @@ -487,7 +487,7 @@ class LocalAdsManagementBloc state.copyWith( nativeAds: updatedAds, snackbarMessage: null, - exception: null, // Clear any previous exception + exception: null, ), ); } else if (item is LocalBannerAd) { @@ -498,7 +498,7 @@ class LocalAdsManagementBloc state.copyWith( bannerAds: updatedAds, snackbarMessage: null, - exception: null, // Clear any previous exception + exception: null, ), ); } else if (item is LocalInterstitialAd) { @@ -510,7 +510,7 @@ class LocalAdsManagementBloc state.copyWith( interstitialAds: updatedAds, snackbarMessage: null, - exception: null, // Clear any previous exception + exception: null, ), ); } else if (item is LocalVideoAd) { @@ -521,7 +521,7 @@ class LocalAdsManagementBloc state.copyWith( videoAds: updatedAds, snackbarMessage: null, - exception: null, // Clear any previous exception + exception: null, ), ); } diff --git a/lib/local_ads_management/view/banner_ads_page.dart b/lib/local_ads_management/view/banner_ads_page.dart index 96300226..f1ad0bb7 100644 --- a/lib/local_ads_management/view/banner_ads_page.dart +++ b/lib/local_ads_management/view/banner_ads_page.dart @@ -6,7 +6,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localiz import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; // Import the new action buttons widget +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -225,7 +225,7 @@ class _BannerAdsDataSource extends DataTableSource { ), ), DataCell( - LocalAdActionButtons(item: ad, l10n: l10n), // Use the new widget + LocalAdActionButtons(item: ad, l10n: l10n), ), ], ); diff --git a/lib/local_ads_management/view/interstitial_ads_page.dart b/lib/local_ads_management/view/interstitial_ads_page.dart index 6bb284d6..8f153c0f 100644 --- a/lib/local_ads_management/view/interstitial_ads_page.dart +++ b/lib/local_ads_management/view/interstitial_ads_page.dart @@ -6,7 +6,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localiz import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; // Import the new action buttons widget +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -32,7 +32,7 @@ class _InterstitialAdsPageState extends State { limit: kDefaultRowsPerPage, filter: context.read().buildLocalAdsFilterMap( context.read().state.copyWith( - selectedAdType: AdType.interstitial, // Ensure correct ad type is set for filter + selectedAdType: AdType.interstitial, ), ), ), @@ -54,7 +54,7 @@ class _InterstitialAdsPageState extends State { padding: const EdgeInsets.only(top: AppSpacing.sm), child: BlocBuilder( builder: (context, state) { - final filterLocalAdsState = context.watch().state; // Watch filter state + final filterLocalAdsState = context.watch().state; final filtersActive = _areFiltersActive(filterLocalAdsState); if (state.interstitialAdsStatus == LocalAdsManagementStatus.loading && @@ -86,7 +86,8 @@ class _InterstitialAdsPageState extends State { } if (state.interstitialAds.isEmpty) { - if (filtersActive) { // Conditionally show reset button if filters are active + if (filtersActive) { + // Conditionally show reset button if filters are active return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -121,7 +122,8 @@ class _InterstitialAdsPageState extends State { return Column( children: [ - if (state.interstitialAdsStatus == LocalAdsManagementStatus.loading && + if (state.interstitialAdsStatus == + LocalAdsManagementStatus.loading && state.interstitialAds.isNotEmpty) const LinearProgressIndicator(), Expanded( @@ -161,9 +163,12 @@ class _InterstitialAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.interstitial, - ), + context + .read() + .state + .copyWith( + selectedAdType: AdType.interstitial, + ), ), ), ); @@ -222,7 +227,7 @@ class _InterstitialAdsDataSource extends DataTableSource { ), ), DataCell( - LocalAdActionButtons(item: ad, l10n: l10n), // Use the new widget + LocalAdActionButtons(item: ad, l10n: l10n), ), ], ); diff --git a/lib/local_ads_management/view/local_ads_management_page.dart b/lib/local_ads_management/view/local_ads_management_page.dart index 5f7c0d4f..367dc084 100644 --- a/lib/local_ads_management/view/local_ads_management_page.dart +++ b/lib/local_ads_management/view/local_ads_management_page.dart @@ -77,7 +77,7 @@ class _LocalAdsManagementPageState extends State UndoDeleteLocalAdRequested( state.snackbarMessage!.split( '"', - )[1], // Extract ID from message + )[1], ), ); }, diff --git a/lib/local_ads_management/view/native_ads_page.dart b/lib/local_ads_management/view/native_ads_page.dart index dbf9bf9a..df5573eb 100644 --- a/lib/local_ads_management/view/native_ads_page.dart +++ b/lib/local_ads_management/view/native_ads_page.dart @@ -6,7 +6,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localiz import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; // Import the new action buttons widget +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -32,7 +32,7 @@ class _NativeAdsPageState extends State { limit: kDefaultRowsPerPage, filter: context.read().buildLocalAdsFilterMap( context.read().state.copyWith( - selectedAdType: AdType.native, // Ensure correct ad type is set for filter + selectedAdType: AdType.native, ), ), ), @@ -46,10 +46,11 @@ class _NativeAdsPageState extends State { padding: const EdgeInsets.only(top: AppSpacing.sm), child: BlocBuilder( builder: (context, state) { - final filterLocalAdsState = context.watch().state; // Watch filter state - final filtersActive = filterLocalAdsState.searchQuery.isNotEmpty || + final filterLocalAdsState = context.watch().state; + final filtersActive = + filterLocalAdsState.searchQuery.isNotEmpty || filterLocalAdsState.selectedStatus != ContentStatus.active || - filterLocalAdsState.selectedAdType != AdType.native; // Check filters for native ads + filterLocalAdsState.selectedAdType != AdType.native; if (state.nativeAdsStatus == LocalAdsManagementStatus.loading && state.nativeAds.isEmpty) { @@ -80,7 +81,8 @@ class _NativeAdsPageState extends State { } if (state.nativeAds.isEmpty) { - if (filtersActive) { // Conditionally show reset button if filters are active + if (filtersActive) { + // Conditionally show reset button if filters are active return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -155,9 +157,12 @@ class _NativeAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.native, - ), + context + .read() + .state + .copyWith( + selectedAdType: AdType.native, + ), ), ), ); @@ -216,7 +221,7 @@ class _NativeAdsDataSource extends DataTableSource { ), ), DataCell( - LocalAdActionButtons(item: ad, l10n: l10n), // Use the new widget + LocalAdActionButtons(item: ad, l10n: l10n), ), ], ); diff --git a/lib/local_ads_management/view/video_ads_page.dart b/lib/local_ads_management/view/video_ads_page.dart index c8b694d3..90577fbf 100644 --- a/lib/local_ads_management/view/video_ads_page.dart +++ b/lib/local_ads_management/view/video_ads_page.dart @@ -6,7 +6,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localiz import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; // Import the new action buttons widget +import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ad_action_buttons.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/string_truncate.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -32,7 +32,7 @@ class _VideoAdsPageState extends State { limit: kDefaultRowsPerPage, filter: context.read().buildLocalAdsFilterMap( context.read().state.copyWith( - selectedAdType: AdType.video, // Ensure correct ad type is set for filter + selectedAdType: AdType.video, ), ), ), @@ -54,7 +54,7 @@ class _VideoAdsPageState extends State { padding: const EdgeInsets.only(top: AppSpacing.sm), child: BlocBuilder( builder: (context, state) { - final filterLocalAdsState = context.watch().state; // Watch filter state + final filterLocalAdsState = context.watch().state; final filtersActive = _areFiltersActive(filterLocalAdsState); if (state.videoAdsStatus == LocalAdsManagementStatus.loading && @@ -86,7 +86,8 @@ class _VideoAdsPageState extends State { } if (state.videoAds.isEmpty) { - if (filtersActive) { // Conditionally show reset button if filters are active + if (filtersActive) { + // Conditionally show reset button if filters are active return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -128,7 +129,9 @@ class _VideoAdsPageState extends State { child: PaginatedDataTable2( columns: [ DataColumn2( - label: Text(l10n.adVideoUrl), // Changed label to adVideoUrl + label: Text( + l10n.adVideoUrl, + ), size: ColumnSize.L, ), DataColumn2( @@ -161,9 +164,12 @@ class _VideoAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.video, - ), + context + .read() + .state + .copyWith( + selectedAdType: AdType.video, + ), ), ), ); @@ -211,7 +217,7 @@ class _VideoAdsDataSource extends DataTableSource { cells: [ DataCell( Text( - ad.videoUrl.truncate(50), // Changed to videoUrl + ad.videoUrl.truncate(50), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -222,7 +228,7 @@ class _VideoAdsDataSource extends DataTableSource { ), ), DataCell( - LocalAdActionButtons(item: ad, l10n: l10n), // Use the new widget + LocalAdActionButtons(item: ad, l10n: l10n), ), ], ); diff --git a/lib/local_ads_management/widgets/local_ad_action_buttons.dart b/lib/local_ads_management/widgets/local_ad_action_buttons.dart index d7e45e9b..5708c497 100644 --- a/lib/local_ads_management/widgets/local_ad_action_buttons.dart +++ b/lib/local_ads_management/widgets/local_ad_action_buttons.dart @@ -1,7 +1,8 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/widgets/content_action_buttons.dart' show ContentActionButtons; +import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/widgets/content_action_buttons.dart' + show ContentActionButtons; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; @@ -160,7 +161,7 @@ class LocalAdActionButtons extends StatelessWidget { // Local ads are not expected to be 'published' from draft, // but including for completeness. context.read().add( - ArchiveLocalAdRequested(itemId), // Treat publish as archive for now + ArchiveLocalAdRequested(itemId), ); case 'archive': context.read().add( From 4beb770121ab60729721a8a36940a9e4feaa644e Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:39:16 +0100 Subject: [PATCH 32/66] feat(localization): add translations for ad ID copy feature - Add Arabic and English translations for new strings related to copying ad IDs - Include translations for: - "ID copied to clipboard" message - "Copy Ad ID" tooltip - Adjust formatting for existing translations --- lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_ar.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 6 ++++++ lib/l10n/arb/app_ar.arb | 14 +++++++++++--- lib/l10n/arb/app_en.arb | 10 +++++++++- 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 4758ce46..2324ee24 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2803,6 +2803,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Video URL'** String get adVideoUrl; + + /// Snackbar message displayed when an ad ID is successfully copied to the clipboard. + /// + /// In en, this message translates to: + /// **'ID copied to clipboard'** + String get idCopied; + + /// Tooltip for the copy ID action button. + /// + /// In en, this message translates to: + /// **'Copy Ad ID'** + String get copyIdTooltip; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 13d26bd5..6dbd8f9e 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1493,4 +1493,10 @@ class AppLocalizationsAr extends AppLocalizations { @override String get adVideoUrl => 'رابط الفيديو'; + + @override + String get idCopied => 'تم نسخ المعرف إلى الحافظة'; + + @override + String get copyIdTooltip => 'نسخ معرف الإعلان'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 319ac03e..50e9dc1d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1498,4 +1498,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get adVideoUrl => 'Video URL'; + + @override + String get idCopied => 'ID copied to clipboard'; + + @override + String get copyIdTooltip => 'Copy Ad ID'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index c1d801f0..69f1a2ac 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1836,11 +1836,11 @@ "@noResultsWithCurrentFilters": { "description": "Message displayed when no results are found due to active filters, prompting the user to reset them." }, -"aboutIconTooltip": "حول هذه الصفحة", + "aboutIconTooltip": "حول هذه الصفحة", "@aboutIconTooltip": { "description": "Tooltip for the information icon that shows page description." }, -"closeButtonText": "إغلاق", + "closeButtonText": "إغلاق", "@closeButtonText": { "description": "Text for the close button in a dialog." }, @@ -1895,5 +1895,13 @@ "adVideoUrl": "رابط الفيديو", "@adVideoUrl": { "description": "رأس العمود لرابط فيديو الإعلان." + }, + "idCopied": "تم نسخ المعرف إلى الحافظة", + "@idCopied": { + "description": "Snackbar message displayed when an ad ID is successfully copied to the clipboard." + }, + "copyIdTooltip": "نسخ معرف الإعلان", + "@copyIdTooltip": { + "description": "Tooltip for the copy ID action button." } -} +} \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 72da7bf1..9f9088b5 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1891,5 +1891,13 @@ "adVideoUrl": "Video URL", "@adVideoUrl": { "description": "Column header for ad video URL." + }, + "idCopied": "ID copied to clipboard", + "@idCopied": { + "description": "Snackbar message displayed when an ad ID is successfully copied to the clipboard." + }, + "copyIdTooltip": "Copy Ad ID", + "@copyIdTooltip": { + "description": "Tooltip for the copy ID action button." } -} +} \ No newline at end of file From fee9a35e4d7516fd88e773235d68ec6e26a130d3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:40:38 +0100 Subject: [PATCH 33/66] style: format --- lib/app_configuration/widgets/feed_decorator_form.dart | 1 - lib/app_configuration/widgets/user_preference_limits_form.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/app_configuration/widgets/feed_decorator_form.dart b/lib/app_configuration/widgets/feed_decorator_form.dart index 0fcd664f..b9dc73b5 100644 --- a/lib/app_configuration/widgets/feed_decorator_form.dart +++ b/lib/app_configuration/widgets/feed_decorator_form.dart @@ -190,7 +190,6 @@ class _FeedDecoratorFormState extends State controller: _itemsToDisplayController, ), const SizedBox(height: AppSpacing.lg), - // Replaced SegmentedButton with TabBar for role selection Align( alignment: AlignmentDirectional.centerStart, child: SizedBox( diff --git a/lib/app_configuration/widgets/user_preference_limits_form.dart b/lib/app_configuration/widgets/user_preference_limits_form.dart index b862df7b..873ec8e7 100644 --- a/lib/app_configuration/widgets/user_preference_limits_form.dart +++ b/lib/app_configuration/widgets/user_preference_limits_form.dart @@ -149,7 +149,6 @@ class _UserPreferenceLimitsFormState extends State ), ), const SizedBox(height: AppSpacing.lg), - // Replaced SegmentedButton with TabBar for role selection Align( alignment: AlignmentDirectional.centerStart, child: SizedBox( From 547f440101691ac249077dce1fbbd5272107cd32 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:42:31 +0100 Subject: [PATCH 34/66] feat(local_ads_management): add adType filter to LoadLocalAdsRequested event - Add adType parameter to LoadLocalAdsRequested event - Update props list to include adType - Allow filtering local ads by ad type in the bloc --- .../bloc/local_ads_management_event.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/local_ads_management/bloc/local_ads_management_event.dart b/lib/local_ads_management/bloc/local_ads_management_event.dart index b04aab6e..b210c06c 100644 --- a/lib/local_ads_management/bloc/local_ads_management_event.dart +++ b/lib/local_ads_management/bloc/local_ads_management_event.dart @@ -31,6 +31,7 @@ final class LoadLocalAdsRequested extends LocalAdsManagementEvent { this.limit, this.forceRefresh = false, this.filter, + this.adType, }); /// Optional ID to start pagination after. @@ -45,8 +46,17 @@ final class LoadLocalAdsRequested extends LocalAdsManagementEvent { /// Optional filter to apply to the local ads query. final Map? filter; + /// Optional ad type to filter by. + final AdType? adType; + @override - List get props => [startAfterId, limit, forceRefresh, filter]; + List get props => [ + startAfterId, + limit, + forceRefresh, + filter, + adType, + ]; } /// {@template archive_local_ad_requested} From cb0b0f063a9b92cae02c4bf8ad10530a10db249c Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:43:07 +0100 Subject: [PATCH 35/66] refactor(local_ads_management): improve ad filtering and loading - Add adType parameter to LoadLocalAdsRequested event - Implement adType mapping based on active tab - Customize search filtering based on ad type - Update loadLocalAds calls to use new adType parameter --- .../bloc/local_ads_management_bloc.dart | 85 ++++++++++++++----- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/lib/local_ads_management/bloc/local_ads_management_bloc.dart b/lib/local_ads_management/bloc/local_ads_management_bloc.dart index eadb8208..b9fe27b3 100644 --- a/lib/local_ads_management/bloc/local_ads_management_bloc.dart +++ b/lib/local_ads_management/bloc/local_ads_management_bloc.dart @@ -39,7 +39,11 @@ class LocalAdsManagementBloc LoadLocalAdsRequested( limit: kDefaultRowsPerPage, forceRefresh: true, - filter: buildLocalAdsFilterMap(_filterLocalAdsBloc.state), + filter: buildLocalAdsFilterMap( + _filterLocalAdsBloc.state, + currentAdType: _mapTabToAdType(state.activeTab), + ), + adType: _mapTabToAdType(state.activeTab), ), ); }); @@ -49,7 +53,11 @@ class LocalAdsManagementBloc LoadLocalAdsRequested( limit: kDefaultRowsPerPage, forceRefresh: true, - filter: buildLocalAdsFilterMap(_filterLocalAdsBloc.state), + filter: buildLocalAdsFilterMap( + _filterLocalAdsBloc.state, + currentAdType: _mapTabToAdType(state.activeTab), + ), + adType: _mapTabToAdType(state.activeTab), ), ); }); @@ -78,26 +86,52 @@ class LocalAdsManagementBloc return super.close(); } + /// Maps a [LocalAdsManagementTab] to its corresponding [AdType]. + AdType _mapTabToAdType(LocalAdsManagementTab tab) { + switch (tab) { + case LocalAdsManagementTab.native: + return AdType.native; + case LocalAdsManagementTab.banner: + return AdType.banner; + case LocalAdsManagementTab.interstitial: + return AdType.interstitial; + case LocalAdsManagementTab.video: + return AdType.video; + } + } + /// Builds a filter map for local ads from the given filter state. - Map buildLocalAdsFilterMap(FilterLocalAdsState state) { + /// + /// [currentAdType] is used to specify the ad type for filtering. + Map buildLocalAdsFilterMap( + FilterLocalAdsState state, { + required AdType currentAdType, + }) { final filter = {}; if (state.searchQuery.isNotEmpty) { - filter[r'$or'] = [ - { - 'title': {r'$regex': state.searchQuery, r'$options': 'i'}, - }, - { - 'imageUrl': {r'$regex': state.searchQuery, r'$options': 'i'}, - }, - { - 'videoUrl': {r'$regex': state.searchQuery, r'$options': 'i'}, - }, - ]; + // Search only by title for native ads, imageUrl for banner/interstitial, + // and videoUrl for video ads. + switch (currentAdType) { + case AdType.native: + filter[r'$or'] = [ + { + 'title': {r'$regex': state.searchQuery, r'$options': 'i'}, + }, + { + 'subtitle': {r'$regex': state.searchQuery, r'$options': 'i'}, + }, + ]; + case AdType.banner: + case AdType.interstitial: + filter['imageUrl'] = {r'$regex': state.searchQuery, r'$options': 'i'}; + case AdType.video: + filter['videoUrl'] = {r'$regex': state.searchQuery, r'$options': 'i'}; + } } filter['status'] = state.selectedStatus.name; - filter['adType'] = state.selectedAdType.name; + filter['adType'] = currentAdType.name; return filter; } @@ -114,7 +148,8 @@ class LocalAdsManagementBloc Emitter emit, ) async { // Determine current state and emit loading status - final currentFilterAdType = _filterLocalAdsBloc.state.selectedAdType; + final currentFilterAdType = + event.adType ?? _mapTabToAdType(state.activeTab); switch (currentFilterAdType) { case AdType.native: @@ -163,7 +198,11 @@ class LocalAdsManagementBloc final isPaginating = event.startAfterId != null; final paginatedAds = await _localAdsRepository.readAll( filter: - event.filter ?? buildLocalAdsFilterMap(_filterLocalAdsBloc.state), + event.filter ?? + buildLocalAdsFilterMap( + _filterLocalAdsBloc.state, + currentAdType: currentFilterAdType, + ), sort: [const SortOption('updatedAt', SortOrder.desc)], pagination: PaginationOptions( cursor: event.startAfterId, @@ -332,7 +371,11 @@ class LocalAdsManagementBloc LoadLocalAdsRequested( limit: kDefaultRowsPerPage, forceRefresh: true, - filter: buildLocalAdsFilterMap(_filterLocalAdsBloc.state), + filter: buildLocalAdsFilterMap( + _filterLocalAdsBloc.state, + currentAdType: _mapTabToAdType(state.activeTab), + ), + adType: _mapTabToAdType(state.activeTab), ), ); } on HttpException catch (e) { @@ -378,7 +421,11 @@ class LocalAdsManagementBloc LoadLocalAdsRequested( limit: kDefaultRowsPerPage, forceRefresh: true, - filter: buildLocalAdsFilterMap(_filterLocalAdsBloc.state), + filter: buildLocalAdsFilterMap( + _filterLocalAdsBloc.state, + currentAdType: _mapTabToAdType(state.activeTab), + ), + adType: _mapTabToAdType(state.activeTab), ), ); } on HttpException catch (e) { From 9c6edef498d26284f01f82de55166b93f38c365d Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:43:16 +0100 Subject: [PATCH 36/66] refactor(local_ads_management): remove unused ad type filter functionality - Remove FilterLocalAdsAdTypeChanged event handler - Remove selectedAdType field from FilterLocalAdsApplied event - Remove unused ad type related code in the bloc --- .../bloc/filter_local_ads/filter_local_ads_bloc.dart | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart index 0ffbd74d..4183d3cb 100644 --- a/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart +++ b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart @@ -27,7 +27,6 @@ class FilterLocalAdsBloc transformer: debounce(const Duration(milliseconds: 300)), ); on(_onFilterLocalAdsStatusChanged); - on(_onFilterLocalAdsAdTypeChanged); on(_onFilterLocalAdsApplied); on(_onFilterLocalAdsReset); } @@ -50,16 +49,6 @@ class FilterLocalAdsBloc emit(state.copyWith(selectedStatus: event.status)); } - /// Handles changes to the selected ad type. - /// - /// This updates the single selected ad type for the filter. - void _onFilterLocalAdsAdTypeChanged( - FilterLocalAdsAdTypeChanged event, - Emitter emit, - ) { - emit(state.copyWith(selectedAdType: event.adType)); - } - /// Handles the application of all current filter settings. /// /// This event is dispatched when the user explicitly confirms the filters @@ -73,7 +62,6 @@ class FilterLocalAdsBloc state.copyWith( searchQuery: event.searchQuery, selectedStatus: event.selectedStatus, - selectedAdType: event.selectedAdType, ), ); } From dcf926371ebac0f926ae43546aab8c9259292fde Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:43:37 +0100 Subject: [PATCH 37/66] refactor(local_ads_management): remove ad type filter - Remove FilterLocalAdsAdTypeChanged event - Remove selectedAdType parameter from FilterLocalAdsApplied event - Remove adType-related imports and code from related files --- .../filter_local_ads/filter_local_ads_event.dart | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_event.dart b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_event.dart index 1901873a..aa70c574 100644 --- a/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_event.dart +++ b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_event.dart @@ -27,33 +27,20 @@ final class FilterLocalAdsStatusChanged extends FilterLocalAdsEvent { List get props => [status]; } -/// Event to notify the BLoC that the selected ad type has changed. -final class FilterLocalAdsAdTypeChanged extends FilterLocalAdsEvent { - const FilterLocalAdsAdTypeChanged(this.adType); - - final AdType adType; - - @override - List get props => [adType]; -} - /// Event to request applying all current filters. final class FilterLocalAdsApplied extends FilterLocalAdsEvent { const FilterLocalAdsApplied({ required this.searchQuery, required this.selectedStatus, - required this.selectedAdType, }); final String searchQuery; final ContentStatus selectedStatus; - final AdType selectedAdType; @override List get props => [ searchQuery, selectedStatus, - selectedAdType, ]; } From f2f119b226f7859507426fd3685a234bf69b773e Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:43:47 +0100 Subject: [PATCH 38/66] refactor(local_ads_management): remove unused ad type filter - Remove selectedAdType property from FilterLocalAdsState - Remove AdType parameter from FilterLocalAdsState constructor - Remove AdType parameter from FilterLocalAdsState copyWith method - Update FilterLocalAdsState props list to remove selectedAdType --- .../bloc/filter_local_ads/filter_local_ads_state.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_state.dart b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_state.dart index 7123e61f..40838020 100644 --- a/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_state.dart +++ b/lib/local_ads_management/bloc/filter_local_ads/filter_local_ads_state.dart @@ -25,7 +25,6 @@ final class FilterLocalAdsState extends Equatable { this.exception, this.searchQuery = '', this.selectedStatus = ContentStatus.active, - this.selectedAdType = AdType.native, }); /// The current status of the filter dialog's main operations. @@ -40,23 +39,18 @@ final class FilterLocalAdsState extends Equatable { /// The single content status to be included in the filter. final ContentStatus selectedStatus; - /// The single ad type to be included in the filter. - final AdType selectedAdType; - /// Creates a copy of this [FilterLocalAdsState] with updated values. FilterLocalAdsState copyWith({ FilterLocalAdsStatus? status, HttpException? exception, String? searchQuery, ContentStatus? selectedStatus, - AdType? selectedAdType, }) { return FilterLocalAdsState( status: status ?? this.status, exception: exception, searchQuery: searchQuery ?? this.searchQuery, selectedStatus: selectedStatus ?? this.selectedStatus, - selectedAdType: selectedAdType ?? this.selectedAdType, ); } @@ -66,6 +60,5 @@ final class FilterLocalAdsState extends Equatable { exception, searchQuery, selectedStatus, - selectedAdType, ]; } From 19d4a3e12cdbd5c3b869b5067db5421eab636a2f Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:43:59 +0100 Subject: [PATCH 39/66] refactor(local_ads_management): update banner ads page filtering logic - Remove unnecessary selectedAdType parameter from FilterLocalAdsState - Update buildLocalAdsFilterMap calls to use currentAdType and adType parameters - Adjust _areFiltersActive logic to reflect removed selectedAdType --- .../view/banner_ads_page.dart | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/local_ads_management/view/banner_ads_page.dart b/lib/local_ads_management/view/banner_ads_page.dart index f1ad0bb7..e98b61eb 100644 --- a/lib/local_ads_management/view/banner_ads_page.dart +++ b/lib/local_ads_management/view/banner_ads_page.dart @@ -31,10 +31,10 @@ class _BannerAdsPageState extends State { LoadLocalAdsRequested( limit: kDefaultRowsPerPage, filter: context.read().buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.banner, - ), + context.read().state, + currentAdType: AdType.banner, ), + adType: AdType.banner, ), ); } @@ -43,8 +43,7 @@ class _BannerAdsPageState extends State { /// for the banner ad type. bool _areFiltersActive(FilterLocalAdsState state) { return state.searchQuery.isNotEmpty || - state.selectedStatus != ContentStatus.active || - state.selectedAdType != AdType.banner; + state.selectedStatus != ContentStatus.active; } @override @@ -76,10 +75,10 @@ class _BannerAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.banner, - ), + context.read().state, + currentAdType: AdType.banner, ), + adType: AdType.banner, ), ), ); @@ -106,7 +105,6 @@ class _BannerAdsPageState extends State { const FilterLocalAdsApplied( searchQuery: '', selectedStatus: ContentStatus.active, - selectedAdType: AdType.banner, ), ); }, @@ -161,13 +159,10 @@ class _BannerAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context - .read() - .state - .copyWith( - selectedAdType: AdType.banner, - ), + context.read().state, + currentAdType: AdType.banner, ), + adType: AdType.banner, ), ); } From 208c9297f37ad24488eb9a61cffa3961ffae5b3f Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:44:11 +0100 Subject: [PATCH 40/66] refactor(local_ads_management): improve interstitial ads filtering - Remove unnecessary selectedAdType parameter from FilterLocalAdsState - Simplify ad type handling in InterstitialAdsPage - Update filter logic to use currentAdType instead of selectedAdType - Adjust FilterLocalAdsApplied event to remove selectedAdType --- .../view/interstitial_ads_page.dart | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/local_ads_management/view/interstitial_ads_page.dart b/lib/local_ads_management/view/interstitial_ads_page.dart index 8f153c0f..bdefdff4 100644 --- a/lib/local_ads_management/view/interstitial_ads_page.dart +++ b/lib/local_ads_management/view/interstitial_ads_page.dart @@ -31,10 +31,10 @@ class _InterstitialAdsPageState extends State { LoadLocalAdsRequested( limit: kDefaultRowsPerPage, filter: context.read().buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.interstitial, - ), + context.read().state, + currentAdType: AdType.interstitial, ), + adType: AdType.interstitial, ), ); } @@ -43,8 +43,7 @@ class _InterstitialAdsPageState extends State { /// for the interstitial ad type. bool _areFiltersActive(FilterLocalAdsState state) { return state.searchQuery.isNotEmpty || - state.selectedStatus != ContentStatus.active || - state.selectedAdType != AdType.interstitial; + state.selectedStatus != ContentStatus.active; } @override @@ -76,10 +75,10 @@ class _InterstitialAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.interstitial, - ), + context.read().state, + currentAdType: AdType.interstitial, ), + adType: AdType.interstitial, ), ), ); @@ -107,7 +106,6 @@ class _InterstitialAdsPageState extends State { const FilterLocalAdsApplied( searchQuery: '', selectedStatus: ContentStatus.active, - selectedAdType: AdType.interstitial, ), ); }, @@ -163,13 +161,10 @@ class _InterstitialAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context - .read() - .state - .copyWith( - selectedAdType: AdType.interstitial, - ), + context.read().state, + currentAdType: AdType.interstitial, ), + adType: AdType.interstitial, ), ); } From eafcbcd9674a8e49844ee930a918db831de5d004 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:44:23 +0100 Subject: [PATCH 41/66] feat(local_ads_management): replace add icon button with floating action button - Remove IconButton from appBar actions - Add FloatingActionButton to scaffold - Move ad creation navigation logic from app bar to floating action button --- .../view/local_ads_management_page.dart | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/local_ads_management/view/local_ads_management_page.dart b/lib/local_ads_management/view/local_ads_management_page.dart index 367dc084..063e37c6 100644 --- a/lib/local_ads_management/view/local_ads_management_page.dart +++ b/lib/local_ads_management/view/local_ads_management_page.dart @@ -114,26 +114,6 @@ class _LocalAdsManagementPageState extends State ); }, ), - IconButton( - icon: const Icon(Icons.add), - tooltip: l10n.addNewItem, - onPressed: () { - final currentTab = context - .read() - .state - .activeTab; - switch (currentTab) { - case LocalAdsManagementTab.native: - context.goNamed(Routes.createLocalNativeAdName); - case LocalAdsManagementTab.banner: - context.goNamed(Routes.createLocalBannerAdName); - case LocalAdsManagementTab.interstitial: - context.goNamed(Routes.createLocalInterstitialAdName); - case LocalAdsManagementTab.video: - context.goNamed(Routes.createLocalVideoAdName); - } - }, - ), const SizedBox(width: AppSpacing.md), ], ), @@ -146,6 +126,25 @@ class _LocalAdsManagementPageState extends State VideoAdsPage(), ], ), + floatingActionButton: FloatingActionButton( + onPressed: () { + final currentTab = context + .read() + .state + .activeTab; + switch (currentTab) { + case LocalAdsManagementTab.native: + context.goNamed(Routes.createLocalNativeAdName); + case LocalAdsManagementTab.banner: + context.goNamed(Routes.createLocalBannerAdName); + case LocalAdsManagementTab.interstitial: + context.goNamed(Routes.createLocalInterstitialAdName); + case LocalAdsManagementTab.video: + context.goNamed(Routes.createLocalVideoAdName); + } + }, + child: const Icon(Icons.add), + ), ), ); } From 1cf570fe3b878fd2cc018043d65c9c3773b8c95e Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:44:42 +0100 Subject: [PATCH 42/66] refactor(local_ads_management): improve native ads filtering - Update LoadLocalAdsRequested event to include adType parameter - Remove selectedAdType from FilterLocalAdsApplied event - Adjust filter logic to use currentAdType instead of selectedAdType - Remove unnecessary selectedAdType checks in the code --- .../view/native_ads_page.dart | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/local_ads_management/view/native_ads_page.dart b/lib/local_ads_management/view/native_ads_page.dart index df5573eb..e56b3ca7 100644 --- a/lib/local_ads_management/view/native_ads_page.dart +++ b/lib/local_ads_management/view/native_ads_page.dart @@ -31,10 +31,10 @@ class _NativeAdsPageState extends State { LoadLocalAdsRequested( limit: kDefaultRowsPerPage, filter: context.read().buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.native, - ), + context.read().state, + currentAdType: AdType.native, ), + adType: AdType.native, ), ); } @@ -49,8 +49,7 @@ class _NativeAdsPageState extends State { final filterLocalAdsState = context.watch().state; final filtersActive = filterLocalAdsState.searchQuery.isNotEmpty || - filterLocalAdsState.selectedStatus != ContentStatus.active || - filterLocalAdsState.selectedAdType != AdType.native; + filterLocalAdsState.selectedStatus != ContentStatus.active; if (state.nativeAdsStatus == LocalAdsManagementStatus.loading && state.nativeAds.isEmpty) { @@ -71,10 +70,10 @@ class _NativeAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.native, - ), + context.read().state, + currentAdType: AdType.native, ), + adType: AdType.native, ), ), ); @@ -102,7 +101,6 @@ class _NativeAdsPageState extends State { const FilterLocalAdsApplied( searchQuery: '', selectedStatus: ContentStatus.active, - selectedAdType: AdType.native, ), ); }, @@ -157,13 +155,10 @@ class _NativeAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context - .read() - .state - .copyWith( - selectedAdType: AdType.native, - ), + context.read().state, + currentAdType: AdType.native, ), + adType: AdType.native, ), ); } From eb40586a6ea2086639980ecf89df7d1aa894e2b8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:44:53 +0100 Subject: [PATCH 43/66] refactor(local_ads_management): update video ads page filtering logic - Remove unnecessary selectedAdType parameter from FilterLocalAdsState - Add currentAdType parameter to buildLocalAdsFilterMap method - Update adType parameter in LoadLocalAdsRequested event - Adjust _areFiltersActive method to remove selectedAdType check --- .../view/video_ads_page.dart | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/local_ads_management/view/video_ads_page.dart b/lib/local_ads_management/view/video_ads_page.dart index 90577fbf..13d397db 100644 --- a/lib/local_ads_management/view/video_ads_page.dart +++ b/lib/local_ads_management/view/video_ads_page.dart @@ -31,10 +31,10 @@ class _VideoAdsPageState extends State { LoadLocalAdsRequested( limit: kDefaultRowsPerPage, filter: context.read().buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.video, - ), + context.read().state, + currentAdType: AdType.video, ), + adType: AdType.video, ), ); } @@ -43,8 +43,7 @@ class _VideoAdsPageState extends State { /// for the video ad type. bool _areFiltersActive(FilterLocalAdsState state) { return state.searchQuery.isNotEmpty || - state.selectedStatus != ContentStatus.active || - state.selectedAdType != AdType.video; + state.selectedStatus != ContentStatus.active; } @override @@ -76,10 +75,10 @@ class _VideoAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context.read().state.copyWith( - selectedAdType: AdType.video, - ), + context.read().state, + currentAdType: AdType.video, ), + adType: AdType.video, ), ), ); @@ -107,7 +106,6 @@ class _VideoAdsPageState extends State { const FilterLocalAdsApplied( searchQuery: '', selectedStatus: ContentStatus.active, - selectedAdType: AdType.video, ), ); }, @@ -164,13 +162,10 @@ class _VideoAdsPageState extends State { filter: context .read() .buildLocalAdsFilterMap( - context - .read() - .state - .copyWith( - selectedAdType: AdType.video, - ), + context.read().state, + currentAdType: AdType.video, ), + adType: AdType.video, ), ); } From 815af406c72385dd41a5c8bb67679f3ec67eb602 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:45:05 +0100 Subject: [PATCH 44/66] feat(local_ads_management): add copy ID option for active ads - Add "Copy ID" option to the overflow menu for active ads - Implement functionality to copy ad ID to clipboard - Display snackbar confirmation after copying ID --- .../widgets/local_ad_action_buttons.dart | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/local_ads_management/widgets/local_ad_action_buttons.dart b/lib/local_ads_management/widgets/local_ad_action_buttons.dart index 5708c497..71bbb664 100644 --- a/lib/local_ads_management/widgets/local_ad_action_buttons.dart +++ b/lib/local_ads_management/widgets/local_ad_action_buttons.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/widgets/content_action_buttons.dart' show ContentActionButtons; @@ -106,6 +107,19 @@ class LocalAdActionButtons extends StatelessWidget { ), ); case ContentStatus.active: + // Add "Copy ID" option for active ads + overflowMenuItems.add( + PopupMenuItem( + value: 'copyId', + child: Row( + children: [ + const Icon(Icons.copy), + const SizedBox(width: AppSpacing.sm), + Text(l10n.copyId), + ], + ), + ), + ); overflowMenuItems.add( PopupMenuItem( value: 'archive', @@ -163,6 +177,15 @@ class LocalAdActionButtons extends StatelessWidget { context.read().add( ArchiveLocalAdRequested(itemId), ); + case 'copyId': + // Copy the ad ID to the clipboard + Clipboard.setData(ClipboardData(text: itemId)).then((_) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.idCopied), + ), + ); + }); case 'archive': context.read().add( ArchiveLocalAdRequested(itemId), From 90779accfd5dd8ba848b19dc0267c2370aace2a5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:45:17 +0100 Subject: [PATCH 45/66] refactor(local_ads_management): remove ad type filter from local ads dialog - Remove ad type filter functionality from LocalAdsFilterDialog - Delete ad type filter chips and related logic - Update dialog documentation to reflect removal of ad type filter - Adjust layout by removing spacing and section related to ad type filter --- .../local_ads_filter_dialog.dart | 58 +------------------ 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/lib/local_ads_management/widgets/local_ads_filter_dialog/local_ads_filter_dialog.dart b/lib/local_ads_management/widgets/local_ads_filter_dialog/local_ads_filter_dialog.dart index 2987cd6c..060e7892 100644 --- a/lib/local_ads_management/widgets/local_ads_filter_dialog/local_ads_filter_dialog.dart +++ b/lib/local_ads_management/widgets/local_ads_filter_dialog/local_ads_filter_dialog.dart @@ -5,15 +5,13 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localiz import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/filter_local_ads/filter_local_ads_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/ad_type_l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/content_status_l10n.dart'; import 'package:ui_kit/ui_kit.dart'; /// {@template local_ads_filter_dialog} /// A full-screen dialog for applying filters to local ads lists. /// -/// This dialog provides a search text field and filter chips for content status -/// and ad type. +/// This dialog provides a search text field and filter chips for content status. /// {@endtemplate} class LocalAdsFilterDialog extends StatefulWidget { /// {@macro local_ads_filter_dialog} @@ -80,7 +78,6 @@ class _LocalAdsFilterDialogState extends State { FilterLocalAdsApplied( searchQuery: resetState.searchQuery, selectedStatus: resetState.selectedStatus, - selectedAdType: resetState.selectedAdType, ), ); Navigator.of(context).pop(); @@ -94,7 +91,6 @@ class _LocalAdsFilterDialogState extends State { FilterLocalAdsApplied( searchQuery: filterDialogState.searchQuery, selectedStatus: filterDialogState.selectedStatus, - selectedAdType: filterDialogState.selectedAdType, ), ); Navigator.of(context).pop(); @@ -129,13 +125,6 @@ class _LocalAdsFilterDialogState extends State { ), const SizedBox(height: AppSpacing.sm), _buildStatusFilterChips(l10n, theme, filterDialogState), - const SizedBox(height: AppSpacing.lg), - Text( - l10n.adType, - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: AppSpacing.sm), - _buildAdTypeFilterChips(l10n, theme, filterDialogState), ], ), ), @@ -174,49 +163,4 @@ class _LocalAdsFilterDialogState extends State { }).toList(), ); } - - /// Builds the ad type filter chips. - Widget _buildAdTypeFilterChips( - AppLocalizations l10n, - ThemeData theme, - LocalAdsFilterDialogState filterDialogState, - ) { - return Wrap( - spacing: AppSpacing.sm, - children: AdType.values.map((adType) { - return ChoiceChip( - avatar: Icon(_getAdTypeIcon(adType)), - label: Text(adType.l10n(context)), - selected: filterDialogState.selectedAdType == adType, - onSelected: (isSelected) { - if (isSelected) { - context.read().add( - LocalAdsFilterDialogAdTypeChanged(adType), - ); - } - }, - selectedColor: theme.colorScheme.primaryContainer, - labelStyle: TextStyle( - color: filterDialogState.selectedAdType == adType - ? theme.colorScheme.onPrimaryContainer - : theme.colorScheme.onSurface, - ), - ); - }).toList(), - ); - } - - /// Returns the appropriate icon for a given AdType. - IconData _getAdTypeIcon(AdType adType) { - switch (adType) { - case AdType.native: - return Icons.article; - case AdType.banner: - return Icons.view_carousel; - case AdType.interstitial: - return Icons.fullscreen; - case AdType.video: - return Icons.videocam; - } - } } From f4209f821a52ec1a59708978863502a6ab2c2d9a Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 17:45:34 +0100 Subject: [PATCH 46/66] refactor(local_ads_management): remove unused ad type filter - Remove LocalAdsFilterDialogAdTypeChanged event handler - Remove selectedAdType property from LocalAdsFilterDialogState - Remove ad type related code from the bloc --- .../bloc/local_ads_filter_dialog_bloc.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart b/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart index 0b70d722..3ce9196f 100644 --- a/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart +++ b/lib/local_ads_management/widgets/local_ads_filter_dialog/bloc/local_ads_filter_dialog_bloc.dart @@ -30,7 +30,6 @@ class LocalAdsFilterDialogBloc transformer: debounce(const Duration(milliseconds: 300)), ); on(_onLocalAdsFilterDialogStatusChanged); - on(_onLocalAdsFilterDialogAdTypeChanged); on(_onLocalAdsFilterDialogReset); } @@ -44,7 +43,6 @@ class LocalAdsFilterDialogBloc state.copyWith( searchQuery: filterState.searchQuery, selectedStatus: filterState.selectedStatus, - selectedAdType: filterState.selectedAdType, ), ); } @@ -65,14 +63,6 @@ class LocalAdsFilterDialogBloc emit(state.copyWith(selectedStatus: event.status)); } - /// Updates the temporary selected ad type. - void _onLocalAdsFilterDialogAdTypeChanged( - LocalAdsFilterDialogAdTypeChanged event, - Emitter emit, - ) { - emit(state.copyWith(selectedAdType: event.adType)); - } - /// Resets all temporary filter selections in the dialog to their initial state. void _onLocalAdsFilterDialogReset( LocalAdsFilterDialogReset event, From b11ff6ed3d84a3c4031ece21a8e8152820d292b5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 19:52:38 +0100 Subject: [PATCH 47/66] feat(local_ads_management): add events for saving and publishing interstitial ads - Add CreateLocalInterstitialAdSavedAsDraft event for saving ads as drafts - Add CreateLocalInterstitialAdPublished event for publishing ads - Remove unnecessary CreateLocalInterstitialAdSubmitted event --- .../create_local_interstitial_ad_event.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_event.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_event.dart index 63f2b287..d843284b 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_event.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_event.dart @@ -37,11 +37,20 @@ final class CreateLocalInterstitialAdTargetUrlChanged List get props => [targetUrl]; } -/// {@template create_local_interstitial_ad_submitted} -/// Event to request submission of the new local interstitial ad. +/// {@template create_local_interstitial_ad_saved_as_draft} +/// Event to request saving the new local interstitial ad as a draft. /// {@endtemplate} -final class CreateLocalInterstitialAdSubmitted +final class CreateLocalInterstitialAdSavedAsDraft extends CreateLocalInterstitialAdEvent { - /// {@macro create_local_interstitial_ad_submitted} - const CreateLocalInterstitialAdSubmitted(); + /// {@macro create_local_interstitial_ad_saved_as_draft} + const CreateLocalInterstitialAdSavedAsDraft(); +} + +/// {@template create_local_interstitial_ad_published} +/// Event to request publishing the new local interstitial ad. +/// {@endtemplate} +final class CreateLocalInterstitialAdPublished + extends CreateLocalInterstitialAdEvent { + /// {@macro create_local_interstitial_ad_published} + const CreateLocalInterstitialAdPublished(); } From 0c4f1a876a728676a1bca2d9df6669af3be9e769 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 19:52:50 +0100 Subject: [PATCH 48/66] feat(local_ads_management): add contentStatus to CreateLocalInterstitialAdState - Add ContentStatus enum to track ad status (draft or active) - Update CreateLocalInterstitialAdState constructor and copyWith method - Include contentStatus in the list of properties for Equatable --- .../create_local_interstitial_ad_state.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_state.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_state.dart index d21f6364..10863d37 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_state.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_state.dart @@ -27,6 +27,7 @@ class CreateLocalInterstitialAdState extends Equatable { this.targetUrl = '', this.exception, this.createdLocalInterstitialAd, + this.contentStatus = ContentStatus.draft, }); /// The current status of the form submission. @@ -44,6 +45,9 @@ class CreateLocalInterstitialAdState extends Equatable { /// The local interstitial ad created upon successful submission. final LocalInterstitialAd? createdLocalInterstitialAd; + /// The content status of the ad (draft or active). + final ContentStatus contentStatus; + /// Returns true if the form is valid, false otherwise. bool get isFormValid => imageUrl.isNotEmpty && targetUrl.isNotEmpty; @@ -54,6 +58,7 @@ class CreateLocalInterstitialAdState extends Equatable { String? targetUrl, HttpException? exception, LocalInterstitialAd? createdLocalInterstitialAd, + ContentStatus? contentStatus, }) { return CreateLocalInterstitialAdState( status: status ?? this.status, @@ -62,6 +67,7 @@ class CreateLocalInterstitialAdState extends Equatable { exception: exception ?? this.exception, createdLocalInterstitialAd: createdLocalInterstitialAd ?? this.createdLocalInterstitialAd, + contentStatus: contentStatus ?? this.contentStatus, ); } @@ -72,5 +78,6 @@ class CreateLocalInterstitialAdState extends Equatable { targetUrl, exception, createdLocalInterstitialAd, + contentStatus, ]; } From dc7c5ca6a107a5d08f127d91804ef87cf56d31c7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 19:53:03 +0100 Subject: [PATCH 49/66] feat(local_ads_management): add handlers for saving and publishing local interstitial ads - Remove _onSubmitted handler - Add _onSavedAsDraft handler for saving ad as a draft - Add _onPublished handler for publishing ad - Update event names in CreateLocalInterstitialAdBloc class --- .../create_local_interstitial_ad_bloc.dart | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_bloc.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_bloc.dart index 4694d3ac..d4e5332e 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_bloc.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_interstitial_ad_bloc.dart @@ -18,7 +18,8 @@ class CreateLocalInterstitialAdBloc super(const CreateLocalInterstitialAdState()) { on(_onImageUrlChanged); on(_onTargetUrlChanged); - on(_onSubmitted); + on(_onSavedAsDraft); + on(_onPublished); } final DataRepository _localAdsRepository; @@ -38,8 +39,52 @@ class CreateLocalInterstitialAdBloc emit(state.copyWith(targetUrl: event.targetUrl)); } - Future _onSubmitted( - CreateLocalInterstitialAdSubmitted event, + /// Handles saving the local interstitial ad as a draft. + Future _onSavedAsDraft( + CreateLocalInterstitialAdSavedAsDraft event, + Emitter emit, + ) async { + if (!state.isFormValid) return; + + emit(state.copyWith(status: CreateLocalInterstitialAdStatus.submitting)); + try { + final now = DateTime.now(); + final newLocalInterstitialAd = LocalInterstitialAd( + id: _uuid.v4(), + imageUrl: state.imageUrl, + targetUrl: state.targetUrl, + createdAt: now, + updatedAt: now, + status: ContentStatus.draft, + ); + + await _localAdsRepository.create(item: newLocalInterstitialAd); + emit( + state.copyWith( + status: CreateLocalInterstitialAdStatus.success, + createdLocalInterstitialAd: newLocalInterstitialAd, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + status: CreateLocalInterstitialAdStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: CreateLocalInterstitialAdStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + /// Handles publishing the local interstitial ad. + Future _onPublished( + CreateLocalInterstitialAdPublished event, Emitter emit, ) async { if (!state.isFormValid) return; From c846e7e4626f1c69ca371b0dff9bb5ff821b2203 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 19:53:22 +0100 Subject: [PATCH 50/66] feat(local_ads_management): add events for saving draft and publishing video ad - Introduce CreateLocalVideoAdSavedAsDraft event for saving video ad as draft - Introduce CreateLocalVideoAdPublished event for publishing video ad - Remove unnecessary CreateLocalVideoAdSubmitted event --- .../create_local_video_ad_event.dart | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_event.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_event.dart index 0d15bc5a..b184c7e5 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_event.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_event.dart @@ -35,10 +35,18 @@ final class CreateLocalVideoAdTargetUrlChanged extends CreateLocalVideoAdEvent { List get props => [targetUrl]; } -/// {@template create_local_video_ad_submitted} -/// Event to request submission of the new local video ad. +/// {@template create_local_video_ad_saved_as_draft} +/// Event to request saving the new local video ad as a draft. /// {@endtemplate} -final class CreateLocalVideoAdSubmitted extends CreateLocalVideoAdEvent { - /// {@macro create_local_video_ad_submitted} - const CreateLocalVideoAdSubmitted(); +final class CreateLocalVideoAdSavedAsDraft extends CreateLocalVideoAdEvent { + /// {@macro create_local_video_ad_saved_as_draft} + const CreateLocalVideoAdSavedAsDraft(); +} + +/// {@template create_local_video_ad_published} +/// Event to request publishing the new local video ad. +/// {@endtemplate} +final class CreateLocalVideoAdPublished extends CreateLocalVideoAdEvent { + /// {@macro create_local_video_ad_published} + const CreateLocalVideoAdPublished(); } From da20e184cf370bef00a799699d85a805a6ad018b Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 19:53:35 +0100 Subject: [PATCH 51/66] feat(local_ads_management): add content status to CreateLocalVideoAdState - Add ContentStatus enum to track draft or active state of the ad - Include contentStatus in CreateLocalVideoAdState properties - Update copyWith method to support contentStatus - Set default contentStatus to ContentStatus.draft in constructor --- .../bloc/create_local_ads/create_local_video_ad_state.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_state.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_state.dart index b68030fb..03b3fd3f 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_state.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_state.dart @@ -27,6 +27,7 @@ class CreateLocalVideoAdState extends Equatable { this.targetUrl = '', this.exception, this.createdLocalVideoAd, + this.contentStatus = ContentStatus.draft, }); /// The current status of the form submission. @@ -44,6 +45,9 @@ class CreateLocalVideoAdState extends Equatable { /// The local video ad created upon successful submission. final LocalVideoAd? createdLocalVideoAd; + /// The content status of the ad (draft or active). + final ContentStatus contentStatus; + /// Returns true if the form is valid, false otherwise. bool get isFormValid => videoUrl.isNotEmpty && targetUrl.isNotEmpty; @@ -54,6 +58,7 @@ class CreateLocalVideoAdState extends Equatable { String? targetUrl, HttpException? exception, LocalVideoAd? createdLocalVideoAd, + ContentStatus? contentStatus, }) { return CreateLocalVideoAdState( status: status ?? this.status, @@ -61,6 +66,7 @@ class CreateLocalVideoAdState extends Equatable { targetUrl: targetUrl ?? this.targetUrl, exception: exception ?? this.exception, createdLocalVideoAd: createdLocalVideoAd ?? this.createdLocalVideoAd, + contentStatus: contentStatus ?? this.contentStatus, ); } @@ -71,5 +77,6 @@ class CreateLocalVideoAdState extends Equatable { targetUrl, exception, createdLocalVideoAd, + contentStatus, ]; } From 3abdd7360bd9be691be2b57029893672af7d5e89 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 19:53:46 +0100 Subject: [PATCH 52/66] feat(local_ads_management): add handlers for saving and publishing local video ads - Replace CreateLocalVideoAdSubmitted event with CreateLocalVideoAdSavedAsDraft and CreateLocalVideoAdPublished events - Implement _onSavedAsDraft method to handle saving local video ads as drafts - Implement _onPublished method to handle publishing local video ads - Update event handlers to set appropriate statuses and handle exceptions --- .../create_local_video_ad_bloc.dart | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_bloc.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_bloc.dart index c31217fe..89087284 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_bloc.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_video_ad_bloc.dart @@ -17,7 +17,8 @@ class CreateLocalVideoAdBloc super(const CreateLocalVideoAdState()) { on(_onVideoUrlChanged); on(_onTargetUrlChanged); - on(_onSubmitted); + on(_onSavedAsDraft); + on(_onPublished); } final DataRepository _localAdsRepository; @@ -37,8 +38,52 @@ class CreateLocalVideoAdBloc emit(state.copyWith(targetUrl: event.targetUrl)); } - Future _onSubmitted( - CreateLocalVideoAdSubmitted event, + /// Handles saving the local video ad as a draft. + Future _onSavedAsDraft( + CreateLocalVideoAdSavedAsDraft event, + Emitter emit, + ) async { + if (!state.isFormValid) return; + + emit(state.copyWith(status: CreateLocalVideoAdStatus.submitting)); + try { + final now = DateTime.now(); + final newLocalVideoAd = LocalVideoAd( + id: _uuid.v4(), + videoUrl: state.videoUrl, + targetUrl: state.targetUrl, + createdAt: now, + updatedAt: now, + status: ContentStatus.draft, + ); + + await _localAdsRepository.create(item: newLocalVideoAd); + emit( + state.copyWith( + status: CreateLocalVideoAdStatus.success, + createdLocalVideoAd: newLocalVideoAd, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + status: CreateLocalVideoAdStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: CreateLocalVideoAdStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + /// Handles publishing the local video ad. + Future _onPublished( + CreateLocalVideoAdPublished event, Emitter emit, ) async { if (!state.isFormValid) return; From 042dd12997c843ab237d3aa1495ffabe47ed1926 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 19:59:11 +0100 Subject: [PATCH 53/66] feat(local_ads_management): add events for saving and publishing local banner ads - Introduce CreateLocalBannerAdSavedAsDraft event for saving ads as drafts - Introduce CreateLocalBannerAdPublished event for publishing ads - Remove unnecessary CreateLocalBannerAdSubmitted event --- .../create_local_banner_ad_event.dart | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_event.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_event.dart index b2bf4abe..3290a6ec 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_event.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_event.dart @@ -37,10 +37,18 @@ final class CreateLocalBannerAdTargetUrlChanged List get props => [targetUrl]; } -/// {@template create_local_banner_ad_submitted} -/// Event to request submission of the new local banner ad. +/// {@template create_local_banner_ad_saved_as_draft} +/// Event to request saving the new local banner ad as a draft. /// {@endtemplate} -final class CreateLocalBannerAdSubmitted extends CreateLocalBannerAdEvent { - /// {@macro create_local_banner_ad_submitted} - const CreateLocalBannerAdSubmitted(); +final class CreateLocalBannerAdSavedAsDraft extends CreateLocalBannerAdEvent { + /// {@macro create_local_banner_ad_saved_as_draft} + const CreateLocalBannerAdSavedAsDraft(); +} + +/// {@template create_local_banner_ad_published} +/// Event to request publishing the new local banner ad. +/// {@endtemplate} +final class CreateLocalBannerAdPublished extends CreateLocalBannerAdEvent { + /// {@macro create_local_banner_ad_published} + const CreateLocalBannerAdPublished(); } From 3439f4d656790c8645d89ba5ca93568aa7b5c729 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 19:59:22 +0100 Subject: [PATCH 54/66] feat(local_ads_management): add contentStatus to CreateLocalBannerAdState - Add ContentStatus enum to track draft or active state of the ad - Include contentStatus in CreateLocalBannerAdState properties - Update copyWith method to support contentStatus - Set default contentStatus to ContentStatus.draft in constructor --- .../create_local_ads/create_local_banner_ad_state.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_state.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_state.dart index b2c12c95..6a8413b4 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_state.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_state.dart @@ -27,6 +27,7 @@ class CreateLocalBannerAdState extends Equatable { this.targetUrl = '', this.exception, this.createdLocalBannerAd, + this.contentStatus = ContentStatus.draft, }); /// The current status of the form submission. @@ -44,6 +45,9 @@ class CreateLocalBannerAdState extends Equatable { /// The local banner ad created upon successful submission. final LocalBannerAd? createdLocalBannerAd; + /// The content status of the ad (draft or active). + final ContentStatus contentStatus; + /// Returns true if the form is valid, false otherwise. bool get isFormValid => imageUrl.isNotEmpty && targetUrl.isNotEmpty; @@ -54,6 +58,7 @@ class CreateLocalBannerAdState extends Equatable { String? targetUrl, HttpException? exception, LocalBannerAd? createdLocalBannerAd, + ContentStatus? contentStatus, }) { return CreateLocalBannerAdState( status: status ?? this.status, @@ -61,6 +66,7 @@ class CreateLocalBannerAdState extends Equatable { targetUrl: targetUrl ?? this.targetUrl, exception: exception ?? this.exception, createdLocalBannerAd: createdLocalBannerAd ?? this.createdLocalBannerAd, + contentStatus: contentStatus ?? this.contentStatus, ); } @@ -71,5 +77,6 @@ class CreateLocalBannerAdState extends Equatable { targetUrl, exception, createdLocalBannerAd, + contentStatus, ]; } From 5a7e615d49b12fbe6ab92e0f7e621d4a13ff3e80 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 19:59:34 +0100 Subject: [PATCH 55/66] feat(local_ads_management): implement save as draft and publish functionalities - Add CreateLocalBannerAdSavedAsDraft and CreateLocalBannerAdPublished events - Implement _onSavedAsDraft and _onPublished event handlers - Update CreateLocalBannerAdStatus to include draft and published states - Modify CreateLocalBannerAdBloc to handle new events --- .../create_local_banner_ad_bloc.dart | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_bloc.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_bloc.dart index daef1e7a..407718be 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_bloc.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_banner_ad_bloc.dart @@ -17,7 +17,8 @@ class CreateLocalBannerAdBloc super(const CreateLocalBannerAdState()) { on(_onImageUrlChanged); on(_onTargetUrlChanged); - on(_onSubmitted); + on(_onSavedAsDraft); + on(_onPublished); } final DataRepository _localAdsRepository; @@ -37,8 +38,52 @@ class CreateLocalBannerAdBloc emit(state.copyWith(targetUrl: event.targetUrl)); } - Future _onSubmitted( - CreateLocalBannerAdSubmitted event, + /// Handles saving the local banner ad as a draft. + Future _onSavedAsDraft( + CreateLocalBannerAdSavedAsDraft event, + Emitter emit, + ) async { + if (!state.isFormValid) return; + + emit(state.copyWith(status: CreateLocalBannerAdStatus.submitting)); + try { + final now = DateTime.now(); + final newLocalBannerAd = LocalBannerAd( + id: _uuid.v4(), + imageUrl: state.imageUrl, + targetUrl: state.targetUrl, + createdAt: now, + updatedAt: now, + status: ContentStatus.draft, + ); + + await _localAdsRepository.create(item: newLocalBannerAd); + emit( + state.copyWith( + status: CreateLocalBannerAdStatus.success, + createdLocalBannerAd: newLocalBannerAd, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + status: CreateLocalBannerAdStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: CreateLocalBannerAdStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + /// Handles publishing the local banner ad. + Future _onPublished( + CreateLocalBannerAdPublished event, Emitter emit, ) async { if (!state.isFormValid) return; From fd9d6fdf865e3a0257a238c4374ab4715e311ef1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:01:11 +0100 Subject: [PATCH 56/66] feat(local_ads_management): add events for saving draft and publishing native ad - Introduce CreateLocalNativeAdSavedAsDraft event for saving native ad as a draft - Add CreateLocalNativeAdPublished event for publishing native ad - Remove unnecessary CreateLocalNativeAdSubmitted event --- .../create_local_native_ad_event.dart | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_event.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_event.dart index edbc2d68..1ebdf34d 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_event.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_event.dart @@ -66,10 +66,18 @@ final class CreateLocalNativeAdTargetUrlChanged List get props => [targetUrl]; } -/// {@template create_local_native_ad_submitted} -/// Event to request submission of the new local native ad. +/// {@template create_local_native_ad_saved_as_draft} +/// Event to request saving the new local native ad as a draft. /// {@endtemplate} -final class CreateLocalNativeAdSubmitted extends CreateLocalNativeAdEvent { - /// {@macro create_local_native_ad_submitted} - const CreateLocalNativeAdSubmitted(); +final class CreateLocalNativeAdSavedAsDraft extends CreateLocalNativeAdEvent { + /// {@macro create_local_native_ad_saved_as_draft} + const CreateLocalNativeAdSavedAsDraft(); +} + +/// {@template create_local_native_ad_published} +/// Event to request publishing the new local native ad. +/// {@endtemplate} +final class CreateLocalNativeAdPublished extends CreateLocalNativeAdEvent { + /// {@macro create_local_native_ad_published} + const CreateLocalNativeAdPublished(); } From d23ac50e1e8a886b53b971353a0378dbd23c6a95 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:01:21 +0100 Subject: [PATCH 57/66] feat(local_ads_management): add content status to native ad creation state - Add ContentStatus enum to track draft or active state of the ad - Update CreateLocalNativeAdState with contentStatus field and default value - Modify copyWith method to include contentStatus parameter - Update props list to include contentStatus for equality checks --- .../create_local_ads/create_local_native_ad_state.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_state.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_state.dart index 350b70a4..3cbe7b40 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_state.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_state.dart @@ -29,6 +29,7 @@ class CreateLocalNativeAdState extends Equatable { this.targetUrl = '', this.exception, this.createdLocalNativeAd, + this.contentStatus = ContentStatus.draft, }); /// The current status of the form submission. @@ -52,6 +53,9 @@ class CreateLocalNativeAdState extends Equatable { /// The local native ad created upon successful submission. final LocalNativeAd? createdLocalNativeAd; + /// The content status of the ad (draft or active). + final ContentStatus contentStatus; + /// Returns true if the form is valid, false otherwise. bool get isFormValid => title.isNotEmpty && @@ -68,6 +72,7 @@ class CreateLocalNativeAdState extends Equatable { String? targetUrl, HttpException? exception, LocalNativeAd? createdLocalNativeAd, + ContentStatus? contentStatus, }) { return CreateLocalNativeAdState( status: status ?? this.status, @@ -77,6 +82,7 @@ class CreateLocalNativeAdState extends Equatable { targetUrl: targetUrl ?? this.targetUrl, exception: exception ?? this.exception, createdLocalNativeAd: createdLocalNativeAd ?? this.createdLocalNativeAd, + contentStatus: contentStatus ?? this.contentStatus, ); } @@ -89,5 +95,6 @@ class CreateLocalNativeAdState extends Equatable { targetUrl, exception, createdLocalNativeAd, + contentStatus, ]; } From e02d66b38478c00543074874b5cdc0da3f6fae8b Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:01:34 +0100 Subject: [PATCH 58/66] feat(local_ads_management): implement save as draft and publish functionalities - Replace CreateLocalNativeAdSubmitted event handler with two new handlers - Add CreateLocalNativeAdSavedAsDraft event handler - Add CreateLocalNativeAdPublished event handler - Implement logic to save local native ad as a draft - Implement logic to publish local native ad - Update state management for both saving as draft and publishing actions --- .../create_local_native_ad_bloc.dart | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_bloc.dart b/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_bloc.dart index 5c1c929c..ee05e593 100644 --- a/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_bloc.dart +++ b/lib/local_ads_management/bloc/create_local_ads/create_local_native_ad_bloc.dart @@ -19,7 +19,8 @@ class CreateLocalNativeAdBloc on(_onSubtitleChanged); on(_onImageUrlChanged); on(_onTargetUrlChanged); - on(_onSubmitted); + on(_onSavedAsDraft); + on(_onPublished); } final DataRepository _localAdsRepository; @@ -53,8 +54,54 @@ class CreateLocalNativeAdBloc emit(state.copyWith(targetUrl: event.targetUrl)); } - Future _onSubmitted( - CreateLocalNativeAdSubmitted event, + /// Handles saving the local native ad as a draft. + Future _onSavedAsDraft( + CreateLocalNativeAdSavedAsDraft event, + Emitter emit, + ) async { + if (!state.isFormValid) return; + + emit(state.copyWith(status: CreateLocalNativeAdStatus.submitting)); + try { + final now = DateTime.now(); + final newLocalNativeAd = LocalNativeAd( + id: _uuid.v4(), + title: state.title, + subtitle: state.subtitle, + imageUrl: state.imageUrl, + targetUrl: state.targetUrl, + createdAt: now, + updatedAt: now, + status: ContentStatus.draft, + ); + + await _localAdsRepository.create(item: newLocalNativeAd); + emit( + state.copyWith( + status: CreateLocalNativeAdStatus.success, + createdLocalNativeAd: newLocalNativeAd, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + status: CreateLocalNativeAdStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: CreateLocalNativeAdStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + /// Handles publishing the local native ad. + Future _onPublished( + CreateLocalNativeAdPublished event, Emitter emit, ) async { if (!state.isFormValid) return; From 49533c200549b61cb72a0ce142d0b5524d48cbbf Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:01:45 +0100 Subject: [PATCH 59/66] feat(local_ads_management): add save options dialog for local banner ads - Implement a dialog to choose between publishing or saving as draft - Update save button functionality to use the new dialog - Add new events to CreateLocalBannerAdBloc for publishing and saving as draft --- .../view/create_local_banner_ad_page.dart | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/local_ads_management/view/create_local_banner_ad_page.dart b/lib/local_ads_management/view/create_local_banner_ad_page.dart index d8cb5228..ca27ab6f 100644 --- a/lib/local_ads_management/view/create_local_banner_ad_page.dart +++ b/lib/local_ads_management/view/create_local_banner_ad_page.dart @@ -54,6 +54,28 @@ class _CreateLocalBannerAdViewState extends State<_CreateLocalBannerAdView> { super.dispose(); } + /// Shows a dialog to the user to choose between publishing or saving as draft. + Future _showSaveOptionsDialog(BuildContext context) async { + final l10n = AppLocalizationsX(context).l10n; + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.saveAdTitle), + content: Text(l10n.saveAdMessage), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(ContentStatus.draft), + child: Text(l10n.saveAsDraft), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(ContentStatus.active), + child: Text(l10n.publish), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -77,9 +99,22 @@ class _CreateLocalBannerAdViewState extends State<_CreateLocalBannerAdView> { icon: const Icon(Icons.save), tooltip: l10n.saveChanges, onPressed: state.isFormValid - ? () => context.read().add( - const CreateLocalBannerAdSubmitted(), - ) + ? () async { + final selectedStatus = await _showSaveOptionsDialog( + context, + ); + if (selectedStatus == ContentStatus.active && + context.mounted) { + context.read().add( + const CreateLocalBannerAdPublished(), + ); + } else if (selectedStatus == ContentStatus.draft && + context.mounted) { + context.read().add( + const CreateLocalBannerAdSavedAsDraft(), + ); + } + } : null, ); }, From ac2f87888022f71a6efa8141739388d313fd2fce Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:01:59 +0100 Subject: [PATCH 60/66] feat(local_ads_management): add option to save interstitial ad as draft or publish - Implement a dialog to let users choose between saving as a draft or publishing the ad - Update save button functionality to handle both draft and publish actions - Add new events to CreateLocalInterstitialAdBloc for handling draft and publish actions --- .../create_local_interstitial_ad_page.dart | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/local_ads_management/view/create_local_interstitial_ad_page.dart b/lib/local_ads_management/view/create_local_interstitial_ad_page.dart index 2ae3f69f..788bdcd1 100644 --- a/lib/local_ads_management/view/create_local_interstitial_ad_page.dart +++ b/lib/local_ads_management/view/create_local_interstitial_ad_page.dart @@ -55,6 +55,28 @@ class _CreateLocalInterstitialAdViewState super.dispose(); } + /// Shows a dialog to the user to choose between publishing or saving as draft. + Future _showSaveOptionsDialog(BuildContext context) async { + final l10n = AppLocalizationsX(context).l10n; + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.saveAdTitle), + content: Text(l10n.saveAdMessage), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(ContentStatus.draft), + child: Text(l10n.saveAsDraft), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(ContentStatus.active), + child: Text(l10n.publish), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -81,9 +103,22 @@ class _CreateLocalInterstitialAdViewState icon: const Icon(Icons.save), tooltip: l10n.saveChanges, onPressed: state.isFormValid - ? () => context.read().add( - const CreateLocalInterstitialAdSubmitted(), - ) + ? () async { + final selectedStatus = await _showSaveOptionsDialog( + context, + ); + if (selectedStatus == ContentStatus.active && + context.mounted) { + context.read().add( + const CreateLocalInterstitialAdPublished(), + ); + } else if (selectedStatus == ContentStatus.draft && + context.mounted) { + context.read().add( + const CreateLocalInterstitialAdSavedAsDraft(), + ); + } + } : null, ); }, From d99dbcd1de92ea362581accd3c947a3a533f2a9f Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:02:09 +0100 Subject: [PATCH 61/66] feat(local_ads_management): add option to save local native ads as draft - Implement dialog for choosing between publishing or saving as draft - Update save button functionality to handle both options - Add new events to CreateLocalNativeAdBloc for publishing and saving as draft --- .../view/create_local_native_ad_page.dart | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/local_ads_management/view/create_local_native_ad_page.dart b/lib/local_ads_management/view/create_local_native_ad_page.dart index 9d4edba6..9936bc82 100644 --- a/lib/local_ads_management/view/create_local_native_ad_page.dart +++ b/lib/local_ads_management/view/create_local_native_ad_page.dart @@ -60,6 +60,28 @@ class _CreateLocalNativeAdViewState extends State<_CreateLocalNativeAdView> { super.dispose(); } + /// Shows a dialog to the user to choose between publishing or saving as draft. + Future _showSaveOptionsDialog(BuildContext context) async { + final l10n = AppLocalizationsX(context).l10n; + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.saveAdTitle), + content: Text(l10n.saveAdMessage), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(ContentStatus.draft), + child: Text(l10n.saveAsDraft), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(ContentStatus.active), + child: Text(l10n.publish), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -83,9 +105,22 @@ class _CreateLocalNativeAdViewState extends State<_CreateLocalNativeAdView> { icon: const Icon(Icons.save), tooltip: l10n.saveChanges, onPressed: state.isFormValid - ? () => context.read().add( - const CreateLocalNativeAdSubmitted(), - ) + ? () async { + final selectedStatus = await _showSaveOptionsDialog( + context, + ); + if (selectedStatus == ContentStatus.active && + context.mounted) { + context.read().add( + const CreateLocalNativeAdPublished(), + ); + } else if (selectedStatus == ContentStatus.draft && + context.mounted) { + context.read().add( + const CreateLocalNativeAdSavedAsDraft(), + ); + } + } : null, ); }, From d550bef5615673fb933262d35d72455ade35aa52 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:02:20 +0100 Subject: [PATCH 62/66] feat(local_ads_management): add option to save local video ad as draft - Implement a dialog to choose between publishing or saving as draft - Update save button functionality to handle both options - Add new events to CreateLocalVideoAdBloc for publishing and saving as draft --- .../view/create_local_video_ad_page.dart | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/local_ads_management/view/create_local_video_ad_page.dart b/lib/local_ads_management/view/create_local_video_ad_page.dart index f6dcc553..64504dc7 100644 --- a/lib/local_ads_management/view/create_local_video_ad_page.dart +++ b/lib/local_ads_management/view/create_local_video_ad_page.dart @@ -54,6 +54,28 @@ class _CreateLocalVideoAdViewState extends State<_CreateLocalVideoAdView> { super.dispose(); } + /// Shows a dialog to the user to choose between publishing or saving as draft. + Future _showSaveOptionsDialog(BuildContext context) async { + final l10n = AppLocalizationsX(context).l10n; + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.saveAdTitle), + content: Text(l10n.saveAdMessage), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(ContentStatus.draft), + child: Text(l10n.saveAsDraft), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(ContentStatus.active), + child: Text(l10n.publish), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -77,9 +99,22 @@ class _CreateLocalVideoAdViewState extends State<_CreateLocalVideoAdView> { icon: const Icon(Icons.save), tooltip: l10n.saveChanges, onPressed: state.isFormValid - ? () => context.read().add( - const CreateLocalVideoAdSubmitted(), - ) + ? () async { + final selectedStatus = await _showSaveOptionsDialog( + context, + ); + if (selectedStatus == ContentStatus.active && + context.mounted) { + context.read().add( + const CreateLocalVideoAdPublished(), + ); + } else if (selectedStatus == ContentStatus.draft && + context.mounted) { + context.read().add( + const CreateLocalVideoAdSavedAsDraft(), + ); + } + } : null, ); }, From 859e33506f8f8a2b3530911ee25133631b0e6e0a Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:02:33 +0100 Subject: [PATCH 63/66] feat(localization): add Arabic translations for save ad dialog - Add Arabic translations for saveAdTitle and saveAdMessage in app_ar.arb - Add English translations for saveAdTitle and saveAdMessage in app_en.arb --- lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_ar.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 7 +++++++ lib/l10n/arb/app_ar.arb | 8 ++++++++ lib/l10n/arb/app_en.arb | 8 ++++++++ 5 files changed, 41 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2324ee24..61fadf6c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2815,6 +2815,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Copy Ad ID'** String get copyIdTooltip; + + /// Title for the dialog asking to save an ad + /// + /// In en, this message translates to: + /// **'Save Ad'** + String get saveAdTitle; + + /// Message for the dialog asking to save an ad + /// + /// In en, this message translates to: + /// **'Do you want to publish this ad or save it as a draft?'** + String get saveAdMessage; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 6dbd8f9e..369d3342 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1499,4 +1499,10 @@ class AppLocalizationsAr extends AppLocalizations { @override String get copyIdTooltip => 'نسخ معرف الإعلان'; + + @override + String get saveAdTitle => 'حفظ الإعلان'; + + @override + String get saveAdMessage => 'هل تريد نشر هذا الإعلان أم حفظه كمسودة؟'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 50e9dc1d..18ee64ec 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1504,4 +1504,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get copyIdTooltip => 'Copy Ad ID'; + + @override + String get saveAdTitle => 'Save Ad'; + + @override + String get saveAdMessage => + 'Do you want to publish this ad or save it as a draft?'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 69f1a2ac..3db46135 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1903,5 +1903,13 @@ "copyIdTooltip": "نسخ معرف الإعلان", "@copyIdTooltip": { "description": "Tooltip for the copy ID action button." + }, + "saveAdTitle": "حفظ الإعلان", + "@saveAdTitle": { + "description": "عنوان مربع الحوار الذي يطلب حفظ إعلان" + }, + "saveAdMessage": "هل تريد نشر هذا الإعلان أم حفظه كمسودة؟", + "@saveAdMessage": { + "description": "رسالة مربع الحوار الذي يطلب حفظ إعلان" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 9f9088b5..8b7d9ff8 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1899,5 +1899,13 @@ "copyIdTooltip": "Copy Ad ID", "@copyIdTooltip": { "description": "Tooltip for the copy ID action button." + }, + "saveAdTitle": "Save Ad", + "@saveAdTitle": { + "description": "Title for the dialog asking to save an ad" + }, + "saveAdMessage": "Do you want to publish this ad or save it as a draft?", + "@saveAdMessage": { + "description": "Message for the dialog asking to save an ad" } } \ No newline at end of file From acb23c70f2c2bf7b70a6b5514b97f34699a5d36c Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:30:19 +0100 Subject: [PATCH 64/66] fix(local_ads_management): update action button functionality - Change ArchiveLocalAdRequested to RestoreLocalAdRequested when clicking - Ensure correct action is performed when user clicks on the restore button --- lib/local_ads_management/widgets/local_ad_action_buttons.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/local_ads_management/widgets/local_ad_action_buttons.dart b/lib/local_ads_management/widgets/local_ad_action_buttons.dart index 71bbb664..83425b60 100644 --- a/lib/local_ads_management/widgets/local_ad_action_buttons.dart +++ b/lib/local_ads_management/widgets/local_ad_action_buttons.dart @@ -175,7 +175,7 @@ class LocalAdActionButtons extends StatelessWidget { // Local ads are not expected to be 'published' from draft, // but including for completeness. context.read().add( - ArchiveLocalAdRequested(itemId), + RestoreLocalAdRequested(itemId), ); case 'copyId': // Copy the ad ID to the clipboard From d4bb1b46913744eb5f4e4f7cc05e5e0939a5102d Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:30:31 +0100 Subject: [PATCH 65/66] feat(local_ads_management): add about icon with tooltip and description - Import shared resources for AboutIcon usage - Add AboutIcon to AppBar title row - Set tooltip and description for AboutIcon using localization strings --- .../view/local_ads_management_page.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/local_ads_management/view/local_ads_management_page.dart b/lib/local_ads_management/view/local_ads_management_page.dart index 063e37c6..7bf8761c 100644 --- a/lib/local_ads_management/view/local_ads_management_page.dart +++ b/lib/local_ads_management/view/local_ads_management_page.dart @@ -9,6 +9,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_manage import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/video_ads_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -87,7 +88,19 @@ class _LocalAdsManagementPageState extends State }, child: Scaffold( appBar: AppBar( - title: Text(l10n.localAdsManagementTitle), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.localAdManagementTitle), + const SizedBox( + width: AppSpacing.xs, + ), + AboutIcon( + dialogTitle: l10n.aboutIconTooltip, + dialogDescription: l10n.localAdManagementDescription, + ), + ], + ), bottom: TabBar( controller: _tabController, tabAlignment: TabAlignment.start, From bc4e7e2480ef7558f9f732392c43a753ffadd818 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 26 Sep 2025 20:44:14 +0100 Subject: [PATCH 66/66] fix(app): update icon and text color in navigation rail - Set the icon color to primary in both leadingUnextendedNavRail and leadingExtendedNavRail - Remove unnecessary text color override in leadingExtendedNavRail --- lib/app/view/app_shell.dart | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/app/view/app_shell.dart b/lib/app/view/app_shell.dart index d2fb127c..e26f4839 100644 --- a/lib/app/view/app_shell.dart +++ b/lib/app/view/app_shell.dart @@ -56,21 +56,25 @@ class AppShell extends StatelessWidget { label: l10n.appConfiguration, ), ], - leadingUnextendedNavRail: const Padding( - padding: EdgeInsets.symmetric(vertical: AppSpacing.lg), - child: Icon(Icons.newspaper_outlined), + leadingUnextendedNavRail: Padding( + padding: const EdgeInsets.symmetric(vertical: AppSpacing.lg), + child: Icon( + Icons.newspaper_outlined, + color: theme.colorScheme.primary, + ), ), leadingExtendedNavRail: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Row( children: [ - const Icon(Icons.newspaper_outlined), + Icon( + Icons.newspaper_outlined, + color: theme.colorScheme.primary, + ), const SizedBox(width: AppSpacing.md), Text( l10n.dashboardTitle, - style: theme.textTheme.titleLarge?.copyWith( - color: theme.colorScheme.primary, - ), + style: theme.textTheme.titleLarge, ), ], ),