From 2db254074f312e8aacf72b1555206ae1fe2d7154 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 16:45:05 +0100 Subject: [PATCH 01/12] refactor(local_ads_management): update ArchiveLocalAdsState for snackbar functionality - Remove lastDeletedLocalAd field and related logic - Add lastPendingDeletionId and snackbarLocalAdTitle fields - Update copyWith method to accommodate new fields - Adjust equality and hashCode methods for new state structure --- .../archive_local_ads_state.dart | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) 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 index cc199f75..23b3053e 100644 --- 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 @@ -29,8 +29,9 @@ class ArchiveLocalAdsState extends Equatable { this.videoAdsCursor, this.videoAdsHasMore = false, this.exception, - this.lastDeletedLocalAd, this.restoredLocalAd, + this.lastPendingDeletionId, + this.snackbarLocalAdTitle, }); final ArchiveLocalAdsStatus status; @@ -86,12 +87,18 @@ class ArchiveLocalAdsState extends Equatable { /// The error describing an operation failure, if any. final HttpException? exception; - /// The last deleted local ad, used for undo functionality. - final LocalAd? lastDeletedLocalAd; - /// The last restored local ad, used for triggering UI updates. final LocalAd? restoredLocalAd; + /// 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, @@ -111,10 +118,9 @@ class ArchiveLocalAdsState extends Equatable { String? videoAdsCursor, bool? videoAdsHasMore, HttpException? exception, - LocalAd? lastDeletedLocalAd, LocalAd? restoredLocalAd, - bool clearLastDeletedLocalAd = false, - bool clearRestoredLocalAd = false, + String? lastPendingDeletionId, + String? snackbarLocalAdTitle, }) { return ArchiveLocalAdsState( status: status ?? this.status, @@ -137,13 +143,11 @@ class ArchiveLocalAdsState extends Equatable { videoAds: videoAds ?? this.videoAds, videoAdsCursor: videoAdsCursor ?? this.videoAdsCursor, videoAdsHasMore: videoAdsHasMore ?? this.videoAdsHasMore, - exception: exception ?? this.exception, - lastDeletedLocalAd: clearLastDeletedLocalAd - ? null - : lastDeletedLocalAd ?? this.lastDeletedLocalAd, - restoredLocalAd: clearRestoredLocalAd - ? null - : restoredLocalAd ?? this.restoredLocalAd, + exception: exception, // Explicitly set to null if not provided + restoredLocalAd: + restoredLocalAd, // Explicitly set to null if not provided + lastPendingDeletionId: lastPendingDeletionId, + snackbarLocalAdTitle: snackbarLocalAdTitle, ); } @@ -167,7 +171,8 @@ class ArchiveLocalAdsState extends Equatable { videoAdsCursor, videoAdsHasMore, exception, - lastDeletedLocalAd, restoredLocalAd, + lastPendingDeletionId, + snackbarLocalAdTitle, ]; } From f5fd4b8a00c24bafc02885ea17cd02db190b58cd Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 16:45:24 +0100 Subject: [PATCH 02/12] refactor(local_ads_management): update pending deletions handling - Rename and restructure the _ConfirmDeleteLocalAdRequested event - Implement a new _DeletionServiceStatusChanged event to handle updates from the pending deletions service --- .../archive_local_ads_event.dart | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) 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 index 96e29ff1..a4268847 100644 --- 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 @@ -73,17 +73,12 @@ final class UndoDeleteLocalAdRequested extends ArchiveLocalAdsEvent { const UndoDeleteLocalAdRequested(); } -/// Internal event to confirm the permanent deletion of a local ad after a delay. -final class _ConfirmDeleteLocalAdRequested extends ArchiveLocalAdsEvent { - /// {@macro _confirm_delete_local_ad_requested} - const _ConfirmDeleteLocalAdRequested(this.id, this.adType); +/// Event to handle updates from the pending deletions service. +final class _DeletionServiceStatusChanged extends ArchiveLocalAdsEvent { + const _DeletionServiceStatusChanged(this.event); - /// The ID of the local ad to confirm deletion for. - final String id; - - /// The type of the local ad to confirm deletion for. - final AdType adType; + final DeletionEvent event; @override - List get props => [id, adType]; + List get props => [event]; } From 1a0405d807facb721a245cc8d537ebc1961d1dd9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 16:46:02 +0100 Subject: [PATCH 03/12] refactor(local_ads_management): integrate PendingDeletionsService for undo functionality - Replace local delete timer with PendingDeletionsService for managing pending deletions - Update BLoC to handle deletion events from PendingDeletionsService - Modify state to include last pending deletion ID and snackbar ad title - Refactor deletion and undo logic to use the new service - Update comments and documentation to reflect changes --- .../archive_local_ads_bloc.dart | 413 +++++++++++------- 1 file changed, 244 insertions(+), 169 deletions(-) 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 index 4e1cfaea..a57be472 100644 --- 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 @@ -4,64 +4,59 @@ 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'; import 'package:ui_kit/ui_kit.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, - }) : _localAdsRepository = localAdsRepository, - super(const ArchiveLocalAdsState()) { + required PendingDeletionsService pendingDeletionsService, + }) : _localAdsRepository = localAdsRepository, + _pendingDeletionsService = pendingDeletionsService, + super(const ArchiveLocalAdsState()) { on(_onLoadArchivedLocalAdsRequested); on(_onRestoreLocalAdRequested); on(_onDeleteLocalAdForeverRequested); on(_onUndoDeleteLocalAdRequested); - on<_ConfirmDeleteLocalAdRequested>(_onConfirmDeleteLocalAdRequested); + on<_DeletionServiceStatusChanged>(_onDeletionServiceStatusChanged); - _localAdUpdateSubscription = _localAdsRepository.entityUpdated - .where((type) => type == LocalAd) - .listen((_) { - add( - const LoadArchivedLocalAdsRequested( - adType: AdType.native, - limit: kDefaultRowsPerPage, - ), - ); - add( - const LoadArchivedLocalAdsRequested( - adType: AdType.banner, - limit: kDefaultRowsPerPage, - ), - ); - add( - const LoadArchivedLocalAdsRequested( - adType: AdType.interstitial, - limit: kDefaultRowsPerPage, - ), - ); - add( - const LoadArchivedLocalAdsRequested( - adType: AdType.video, - limit: kDefaultRowsPerPage, - ), - ); - }); + // Listen to deletion events from the PendingDeletionsService. + _deletionEventSubscription = _pendingDeletionsService.deletionEvents.listen( + (event) { + if (event.item is LocalAd) { + add(_DeletionServiceStatusChanged(event)); + } + }, + ); } final DataRepository _localAdsRepository; - late final StreamSubscription _localAdUpdateSubscription; - Timer? _deleteTimer; + final PendingDeletionsService _pendingDeletionsService; + + /// Subscription to deletion events from the PendingDeletionsService. + late final StreamSubscription> + _deletionEventSubscription; @override Future close() { - _localAdUpdateSubscription.cancel(); - _deleteTimer?.cancel(); + _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, @@ -98,9 +93,7 @@ class ArchiveLocalAdsBloc switch (event.adType) { case AdType.native: - final previousAds = isPaginating - ? state.nativeAds - : []; + final previousAds = isPaginating ? state.nativeAds : []; emit( state.copyWith( nativeAdsStatus: ArchiveLocalAdsStatus.success, @@ -113,9 +106,7 @@ class ArchiveLocalAdsBloc ), ); case AdType.banner: - final previousAds = isPaginating - ? state.bannerAds - : []; + final previousAds = isPaginating ? state.bannerAds : []; emit( state.copyWith( bannerAdsStatus: ArchiveLocalAdsStatus.success, @@ -221,10 +212,18 @@ class ArchiveLocalAdsBloc } } + /// 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); @@ -239,13 +238,29 @@ class ArchiveLocalAdsBloc if (index == -1) return; adToRestore = originalNativeAds[index]; originalNativeAds.removeAt(index); - emit(state.copyWith(nativeAds: originalNativeAds)); + 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)); + 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, @@ -253,13 +268,29 @@ class ArchiveLocalAdsBloc if (index == -1) return; adToRestore = originalInterstitialAds[index]; originalInterstitialAds.removeAt(index); - emit(state.copyWith(interstitialAds: originalInterstitialAds)); + 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)); + emit( + state.copyWith( + videoAds: originalVideoAds, + lastPendingDeletionId: state.lastPendingDeletionId == event.id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + ), + ); } try { @@ -283,40 +314,84 @@ class ArchiveLocalAdsBloc // Revert UI on failure switch (event.adType) { case AdType.native: - emit(state.copyWith(nativeAds: originalNativeAds)); + emit( + state.copyWith( + nativeAds: originalNativeAds, + exception: e, + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.banner: - emit(state.copyWith(bannerAds: originalBannerAds)); + emit( + state.copyWith( + bannerAds: originalBannerAds, + exception: e, + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.interstitial: - emit(state.copyWith(interstitialAds: originalInterstitialAds)); + emit( + state.copyWith( + interstitialAds: originalInterstitialAds, + exception: e, + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.video: - emit(state.copyWith(videoAds: originalVideoAds)); + emit( + state.copyWith( + videoAds: originalVideoAds, + exception: e, + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); } - emit(state.copyWith(exception: e)); } catch (e) { switch (event.adType) { case AdType.native: - emit(state.copyWith(nativeAds: originalNativeAds)); + emit( + state.copyWith( + nativeAds: originalNativeAds, + exception: UnknownException('An unexpected error occurred: $e'), + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.banner: - emit(state.copyWith(bannerAds: originalBannerAds)); + emit( + state.copyWith( + bannerAds: originalBannerAds, + exception: UnknownException('An unexpected error occurred: $e'), + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.interstitial: - emit(state.copyWith(interstitialAds: originalInterstitialAds)); + emit( + state.copyWith( + interstitialAds: originalInterstitialAds, + exception: UnknownException('An unexpected error occurred: $e'), + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.video: - emit(state.copyWith(videoAds: originalVideoAds)); + emit( + state.copyWith( + videoAds: originalVideoAds, + exception: UnknownException('An unexpected error occurred: $e'), + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); } - emit( - state.copyWith( - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); } } + /// 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 { - _deleteTimer?.cancel(); - LocalAd? adToDelete; final currentNativeAds = List.from(state.nativeAds); final currentBannerAds = List.from(state.bannerAds); @@ -360,130 +435,130 @@ class ArchiveLocalAdsBloc emit(state.copyWith(videoAds: currentVideoAds)); } - emit(state.copyWith(lastDeletedLocalAd: adToDelete)); + 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, + ), + ); - _deleteTimer = Timer( - const Duration(seconds: 5), - () => add(_ConfirmDeleteLocalAdRequested(event.id, event.adType)), + // Request deletion via the service. + _pendingDeletionsService.requestDeletion( + item: adToDelete, + repository: _localAdsRepository, + undoDuration: const Duration(seconds: 5), ); } - Future _onConfirmDeleteLocalAdRequested( - _ConfirmDeleteLocalAdRequested event, + /// 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 { - 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) { + 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) - ..add(restoredAd as LocalNativeAd); - emit(state.copyWith(nativeAds: updatedAds)); + ..insert(0, item as LocalNativeAd); + emit( + state.copyWith( + nativeAds: updatedAds, + lastPendingDeletionId: state.lastPendingDeletionId == id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + restoredLocalAd: item, + ), + ); case 'banner': final updatedAds = List.from(state.bannerAds) - ..add(restoredAd as LocalBannerAd); - emit(state.copyWith(bannerAds: updatedAds)); + ..insert(0, item as LocalBannerAd); + emit( + state.copyWith( + bannerAds: updatedAds, + lastPendingDeletionId: state.lastPendingDeletionId == id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + restoredLocalAd: item, + ), + ); case 'interstitial': final updatedAds = List.from( state.interstitialAds, - )..add(restoredAd as LocalInterstitialAd); - emit(state.copyWith(interstitialAds: updatedAds)); + )..insert(0, item as LocalInterstitialAd); + emit( + state.copyWith( + interstitialAds: updatedAds, + lastPendingDeletionId: state.lastPendingDeletionId == id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + restoredLocalAd: item, + ), + ); 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': - final updatedAds = List.from(state.nativeAds) - ..insert( - 0, - restoredAd as LocalNativeAd, + ..insert(0, item as LocalVideoAd); + emit( + state.copyWith( + videoAds: updatedAds, + lastPendingDeletionId: state.lastPendingDeletionId == id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + restoredLocalAd: item, + ), ); - emit( - state.copyWith( - nativeAds: updatedAds, - clearLastDeletedLocalAd: true, - restoredLocalAd: restoredAd, - ), - ); - case 'banner': - final updatedAds = List.from(state.bannerAds) - ..insert(0, restoredAd as LocalBannerAd); - emit( - state.copyWith( - bannerAds: updatedAds, - clearLastDeletedLocalAd: true, - restoredLocalAd: restoredAd, - ), - ); - case 'interstitial': - final updatedAds = List.from( - state.interstitialAds, - )..insert(0, restoredAd as LocalInterstitialAd); - emit( - state.copyWith( - interstitialAds: updatedAds, - clearLastDeletedLocalAd: true, - restoredLocalAd: restoredAd, - ), - ); - case 'video': - final updatedAds = List.from(state.videoAds) - ..insert(0, restoredAd as LocalVideoAd); - emit( - state.copyWith( - videoAds: updatedAds, - clearLastDeletedLocalAd: true, - restoredLocalAd: restoredAd, - ), - ); + } } } } From a034f5b9ff8c217b3efedd5072ad3824fa94a91c Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 16:46:15 +0100 Subject: [PATCH 04/12] refactor(local_ads_management): improve archived ads undo delete functionality - Introduce PendingDeletionsService for better undo management - Simplify snackbar logic in ArchivedLocalAdsPage - Update ArchiveLocalAdsBloc to use pending deletions service --- .../view/archived_local_ads_page.dart | 45 ++++--------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/lib/local_ads_management/view/archived_local_ads_page.dart b/lib/local_ads_management/view/archived_local_ads_page.dart index 029b66cb..0e7353f1 100644 --- a/lib/local_ads_management/view/archived_local_ads_page.dart +++ b/lib/local_ads_management/view/archived_local_ads_page.dart @@ -12,6 +12,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_manage RestoreLocalAdRequested, UndoDeleteLocalAdRequested; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -28,6 +29,7 @@ class ArchivedLocalAdsPage extends StatelessWidget { create: (context) => ArchiveLocalAdsBloc( localAdsRepository: context.read>(), + pendingDeletionsService: context.read(), ) ..add(const LoadArchivedLocalAdsRequested(adType: AdType.native)) ..add(const LoadArchivedLocalAdsRequested(adType: AdType.banner)) @@ -77,6 +79,7 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; + final pendingDeletionsService = context.read(); return Scaffold( appBar: AppBar( title: Text(l10n.archivedLocalAdsTitle), @@ -89,42 +92,14 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> ), body: BlocListener( listenWhen: (previous, current) => - previous.lastDeletedLocalAd != current.lastDeletedLocalAd || - (previous.nativeAds.length != current.nativeAds.length && - current.lastDeletedLocalAd == null) || - (previous.bannerAds.length != current.bannerAds.length && - current.lastDeletedLocalAd == null) || - (previous.interstitialAds.length != - current.interstitialAds.length && - current.lastDeletedLocalAd == null) || - (previous.videoAds.length != current.videoAds.length && - current.lastDeletedLocalAd == null) || + previous.lastPendingDeletionId != current.lastPendingDeletionId || + previous.snackbarLocalAdTitle != current.snackbarLocalAdTitle || (previous.restoredLocalAd == null && current.restoredLocalAd != 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); - } - + if (state.snackbarLocalAdTitle != null) { + final adId = state.lastPendingDeletionId!; + final truncatedTitle = state.snackbarLocalAdTitle!.truncate(30); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( @@ -135,9 +110,7 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> action: SnackBarAction( label: l10n.undo, onPressed: () { - context.read().add( - const UndoDeleteLocalAdRequested(), - ); + pendingDeletionsService.undoDeletion(adId); }, ), ), From 5db0abb8ddd5be91e72e5d4b06d09d1bd87b447a Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 16:48:22 +0100 Subject: [PATCH 05/12] fix(archive_local_ads): remove explicit null assignment - Remove unnecessary null assignment for 'exception' and 'restoredLocalAd' - Simplify state copying by removing redundant code --- .../bloc/archive_local_ads/archive_local_ads_state.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 23b3053e..4b1cd096 100644 --- 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 @@ -143,9 +143,8 @@ class ArchiveLocalAdsState extends Equatable { videoAds: videoAds ?? this.videoAds, videoAdsCursor: videoAdsCursor ?? this.videoAdsCursor, videoAdsHasMore: videoAdsHasMore ?? this.videoAdsHasMore, - exception: exception, // Explicitly set to null if not provided - restoredLocalAd: - restoredLocalAd, // Explicitly set to null if not provided + exception: exception, + restoredLocalAd: restoredLocalAd, lastPendingDeletionId: lastPendingDeletionId, snackbarLocalAdTitle: snackbarLocalAdTitle, ); From afc4300bd9e066c29816077f054343fb9a963fe8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 16:49:00 +0100 Subject: [PATCH 06/12] style: format --- .../archive_local_ads_bloc.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 index a57be472..6ef3a736 100644 --- 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 @@ -22,9 +22,9 @@ class ArchiveLocalAdsBloc ArchiveLocalAdsBloc({ required DataRepository localAdsRepository, required PendingDeletionsService pendingDeletionsService, - }) : _localAdsRepository = localAdsRepository, - _pendingDeletionsService = pendingDeletionsService, - super(const ArchiveLocalAdsState()) { + }) : _localAdsRepository = localAdsRepository, + _pendingDeletionsService = pendingDeletionsService, + super(const ArchiveLocalAdsState()) { on(_onLoadArchivedLocalAdsRequested); on(_onRestoreLocalAdRequested); on(_onDeleteLocalAdForeverRequested); @@ -46,7 +46,7 @@ class ArchiveLocalAdsBloc /// Subscription to deletion events from the PendingDeletionsService. late final StreamSubscription> - _deletionEventSubscription; + _deletionEventSubscription; @override Future close() { @@ -93,7 +93,9 @@ class ArchiveLocalAdsBloc switch (event.adType) { case AdType.native: - final previousAds = isPaginating ? state.nativeAds : []; + final previousAds = isPaginating + ? state.nativeAds + : []; emit( state.copyWith( nativeAdsStatus: ArchiveLocalAdsStatus.success, @@ -106,7 +108,9 @@ class ArchiveLocalAdsBloc ), ); case AdType.banner: - final previousAds = isPaginating ? state.bannerAds : []; + final previousAds = isPaginating + ? state.bannerAds + : []; emit( state.copyWith( bannerAdsStatus: ArchiveLocalAdsStatus.success, From 31144609c24db8e1bb0b9007daa428f68e2b7650 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 17:05:41 +0100 Subject: [PATCH 07/12] fix(archive_local_ads): ensure proper null checks for state properties - Update the ArchiveLocalAdsState.copyWith constructor to properly handle null values for exception, restoredLocalAd, lastPendingDeletionId, and snackbarLocalAdTitle properties. - This change prevents potential null dereference errors when these properties are not explicitly provided when copying the state. --- .../bloc/archive_local_ads/archive_local_ads_state.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 4b1cd096..e150591c 100644 --- 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 @@ -143,10 +143,10 @@ class ArchiveLocalAdsState extends Equatable { videoAds: videoAds ?? this.videoAds, videoAdsCursor: videoAdsCursor ?? this.videoAdsCursor, videoAdsHasMore: videoAdsHasMore ?? this.videoAdsHasMore, - exception: exception, - restoredLocalAd: restoredLocalAd, - lastPendingDeletionId: lastPendingDeletionId, - snackbarLocalAdTitle: snackbarLocalAdTitle, + exception: exception ?? this.exception, + restoredLocalAd: restoredLocalAd ?? this.restoredLocalAd, + lastPendingDeletionId: lastPendingDeletionId ?? this.lastPendingDeletionId, + snackbarLocalAdTitle: snackbarLocalAdTitle ?? this.snackbarLocalAdTitle, ); } From 2ae6ccd04d006a7643e67a1ca7dc42cc59583241 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 17:05:54 +0100 Subject: [PATCH 08/12] refactor(local_ads_management): update undo delete action to use bloc - Remove direct dependency on PendingDeletionsService - Use ArchiveLocalAdsBloc to handle undo delete action - Improve adherence to business logic separation principles --- lib/local_ads_management/view/archived_local_ads_page.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/local_ads_management/view/archived_local_ads_page.dart b/lib/local_ads_management/view/archived_local_ads_page.dart index 0e7353f1..27cb58f1 100644 --- a/lib/local_ads_management/view/archived_local_ads_page.dart +++ b/lib/local_ads_management/view/archived_local_ads_page.dart @@ -79,7 +79,6 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; - final pendingDeletionsService = context.read(); return Scaffold( appBar: AppBar( title: Text(l10n.archivedLocalAdsTitle), @@ -110,7 +109,9 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> action: SnackBarAction( label: l10n.undo, onPressed: () { - pendingDeletionsService.undoDeletion(adId); + context.read().add( + const UndoDeleteLocalAdRequested(), + ); }, ), ), From 15daa190f8399f27fffc875212c299f3d4f9cb71 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 17:50:39 +0100 Subject: [PATCH 09/12] fix(ads): remove redundant state updates for restoredLocalAd - Remove unnecessary `restoredLocalAd` updates in multiple states - This change simplifies the state management and improves performance --- .../bloc/archive_local_ads/archive_local_ads_bloc.dart | 5 ----- 1 file changed, 5 deletions(-) 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 index 6ef3a736..0084a738 100644 --- 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 @@ -313,7 +313,6 @@ class ArchiveLocalAdsBloc id: event.id, item: updatedAd, ); - emit(state.copyWith(restoredLocalAd: updatedAd)); } on HttpException catch (e) { // Revert UI on failure switch (event.adType) { @@ -519,7 +518,6 @@ class ArchiveLocalAdsBloc ? null : state.lastPendingDeletionId, snackbarLocalAdTitle: null, - restoredLocalAd: item, ), ); case 'banner': @@ -532,7 +530,6 @@ class ArchiveLocalAdsBloc ? null : state.lastPendingDeletionId, snackbarLocalAdTitle: null, - restoredLocalAd: item, ), ); case 'interstitial': @@ -546,7 +543,6 @@ class ArchiveLocalAdsBloc ? null : state.lastPendingDeletionId, snackbarLocalAdTitle: null, - restoredLocalAd: item, ), ); case 'video': @@ -559,7 +555,6 @@ class ArchiveLocalAdsBloc ? null : state.lastPendingDeletionId, snackbarLocalAdTitle: null, - restoredLocalAd: item, ), ); } From 7533803d8b3b598d51eaf59bb613acb1238a239b Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 17:50:51 +0100 Subject: [PATCH 10/12] refactor(local_ads_management): remove unused restoredLocalAd field - Remove restoredLocalAd field from ArchiveLocalAdsState - Adjust copyWith method to remove restoredLocalAd parameter - Update ArchiveLocalAdsStateProps to exclude restoredLocalAd --- .../archive_local_ads/archive_local_ads_state.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) 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 index e150591c..7c547782 100644 --- 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 @@ -29,7 +29,6 @@ class ArchiveLocalAdsState extends Equatable { this.videoAdsCursor, this.videoAdsHasMore = false, this.exception, - this.restoredLocalAd, this.lastPendingDeletionId, this.snackbarLocalAdTitle, }); @@ -87,9 +86,6 @@ class ArchiveLocalAdsState extends Equatable { /// The error describing an operation failure, if any. final HttpException? exception; - /// The last restored local ad, used for triggering UI updates. - final LocalAd? restoredLocalAd; - /// The ID of the local ad that was most recently added to pending deletions. /// Used to trigger the snackbar display. final String? lastPendingDeletionId; @@ -118,7 +114,6 @@ class ArchiveLocalAdsState extends Equatable { String? videoAdsCursor, bool? videoAdsHasMore, HttpException? exception, - LocalAd? restoredLocalAd, String? lastPendingDeletionId, String? snackbarLocalAdTitle, }) { @@ -143,10 +138,9 @@ class ArchiveLocalAdsState extends Equatable { videoAds: videoAds ?? this.videoAds, videoAdsCursor: videoAdsCursor ?? this.videoAdsCursor, videoAdsHasMore: videoAdsHasMore ?? this.videoAdsHasMore, - exception: exception ?? this.exception, - restoredLocalAd: restoredLocalAd ?? this.restoredLocalAd, + exception: exception, lastPendingDeletionId: lastPendingDeletionId ?? this.lastPendingDeletionId, - snackbarLocalAdTitle: snackbarLocalAdTitle ?? this.snackbarLocalAdTitle, + snackbarLocalAdTitle: snackbarLocalAdTitle, ); } @@ -170,7 +164,6 @@ class ArchiveLocalAdsState extends Equatable { videoAdsCursor, videoAdsHasMore, exception, - restoredLocalAd, lastPendingDeletionId, snackbarLocalAdTitle, ]; From 4fc3846b013caa994c38d4b88da43ff4dee67e4f Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 17:51:03 +0100 Subject: [PATCH 11/12] fix(archived_local_ads_page): Prevent "deactivated widget's ancestor" error - Replace context.read in SnackBar action with direct service call - Remove unnecessary state check for restoredLocalAd - Update BlocListener to avoid redundant UI updates --- .../view/archived_local_ads_page.dart | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/local_ads_management/view/archived_local_ads_page.dart b/lib/local_ads_management/view/archived_local_ads_page.dart index 27cb58f1..fc3b1de3 100644 --- a/lib/local_ads_management/view/archived_local_ads_page.dart +++ b/lib/local_ads_management/view/archived_local_ads_page.dart @@ -79,6 +79,13 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> @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), @@ -92,9 +99,7 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> body: BlocListener( listenWhen: (previous, current) => previous.lastPendingDeletionId != current.lastPendingDeletionId || - previous.snackbarLocalAdTitle != current.snackbarLocalAdTitle || - (previous.restoredLocalAd == null && - current.restoredLocalAd != null), + previous.snackbarLocalAdTitle != current.snackbarLocalAdTitle, listener: (context, state) { if (state.snackbarLocalAdTitle != null) { final adId = state.lastPendingDeletionId!; @@ -109,23 +114,15 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> action: SnackBarAction( label: l10n.undo, onPressed: () { - context.read().add( - const UndoDeleteLocalAdRequested(), - ); + // 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); }, ), ), ); } - // Trigger refresh of active ads in LocalAdsManagementBloc if an ad was restored - if (state.restoredLocalAd != null) { - context.read().add( - LoadLocalAdsRequested( - adType: state.restoredLocalAd!.toAdType(), - forceRefresh: true, - ), - ); - } }, child: TabBarView( controller: _tabController, From 6b5418b33b49a4211f5c438614a3e79b8c5d571a Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 24 Sep 2025 17:53:59 +0100 Subject: [PATCH 12/12] style: format --- lib/app_configuration/widgets/article_ad_settings_form.dart | 2 +- .../bloc/archive_local_ads/archive_local_ads_bloc.dart | 1 - .../bloc/archive_local_ads/archive_local_ads_state.dart | 3 ++- lib/local_ads_management/view/archived_local_ads_page.dart | 5 ----- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/app_configuration/widgets/article_ad_settings_form.dart b/lib/app_configuration/widgets/article_ad_settings_form.dart index 274aae00..03a2bb2b 100644 --- a/lib/app_configuration/widgets/article_ad_settings_form.dart +++ b/lib/app_configuration/widgets/article_ad_settings_form.dart @@ -2,9 +2,9 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/banner_ad_shape_l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/in_article_ad_slot_type_l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; import 'package:ui_kit/ui_kit.dart'; /// {@template article_ad_settings_form} 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 index 0084a738..6e01c649 100644 --- 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 @@ -5,7 +5,6 @@ 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'; -import 'package:ui_kit/ui_kit.dart'; part 'archive_local_ads_event.dart'; part 'archive_local_ads_state.dart'; 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 index 7c547782..7b5d4393 100644 --- 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 @@ -139,7 +139,8 @@ class ArchiveLocalAdsState extends Equatable { videoAdsCursor: videoAdsCursor ?? this.videoAdsCursor, videoAdsHasMore: videoAdsHasMore ?? this.videoAdsHasMore, exception: exception, - lastPendingDeletionId: lastPendingDeletionId ?? this.lastPendingDeletionId, + lastPendingDeletionId: + lastPendingDeletionId ?? this.lastPendingDeletionId, snackbarLocalAdTitle: 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 index fc3b1de3..c5a04956 100644 --- a/lib/local_ads_management/view/archived_local_ads_page.dart +++ b/lib/local_ads_management/view/archived_local_ads_page.dart @@ -6,11 +6,6 @@ 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/local_ads_management/bloc/local_ads_management_bloc.dart' - hide - DeleteLocalAdForeverRequested, - RestoreLocalAdRequested, - UndoDeleteLocalAdRequested; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; import 'package:intl/intl.dart';