Skip to content

Commit 38b74a5

Browse files
authored
Merge pull request #54 from flutter-news-app-full-source-code/feature-undo-delete-in-headlines-archive
Feature undo delete in headlines archive
2 parents 2177cea + 78468ff commit 38b74a5

File tree

11 files changed

+186
-14
lines changed

11 files changed

+186
-14
lines changed

lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:async';
2+
13
import 'package:bloc/bloc.dart';
24
import 'package:core/core.dart';
35
import 'package:data_repository/data_repository.dart';
@@ -15,9 +17,18 @@ class ArchivedHeadlinesBloc
1517
on<LoadArchivedHeadlinesRequested>(_onLoadArchivedHeadlinesRequested);
1618
on<RestoreHeadlineRequested>(_onRestoreHeadlineRequested);
1719
on<DeleteHeadlineForeverRequested>(_onDeleteHeadlineForeverRequested);
20+
on<UndoDeleteHeadlineRequested>(_onUndoDeleteHeadlineRequested);
21+
on<_ConfirmDeleteHeadlineRequested>(_onConfirmDeleteHeadlineRequested);
1822
}
1923

2024
final DataRepository<Headline> _headlinesRepository;
25+
Timer? _deleteTimer;
26+
27+
@override
28+
Future<void> close() {
29+
_deleteTimer?.cancel();
30+
return super.close();
31+
}
2132

2233
Future<void> _onLoadArchivedHeadlinesRequested(
2334
LoadArchivedHeadlinesRequested event,
@@ -96,22 +107,83 @@ class ArchivedHeadlinesBloc
96107
DeleteHeadlineForeverRequested event,
97108
Emitter<ArchivedHeadlinesState> emit,
98109
) async {
99-
final originalHeadlines = List<Headline>.from(state.headlines);
100-
final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id);
110+
_deleteTimer?.cancel();
111+
112+
final headlineIndex = state.headlines.indexWhere((h) => h.id == event.id);
101113
if (headlineIndex == -1) return;
102114

103-
final updatedHeadlines = originalHeadlines..removeAt(headlineIndex);
104-
emit(state.copyWith(headlines: updatedHeadlines));
115+
final headlineToDelete = state.headlines[headlineIndex];
116+
final updatedHeadlines = List<Headline>.from(state.headlines)
117+
..removeAt(headlineIndex);
118+
119+
emit(
120+
state.copyWith(
121+
headlines: updatedHeadlines,
122+
lastDeletedHeadline: headlineToDelete,
123+
),
124+
);
125+
126+
_deleteTimer = Timer(
127+
const Duration(seconds: 5),
128+
() => add(_ConfirmDeleteHeadlineRequested(event.id)),
129+
);
130+
}
105131

132+
Future<void> _onConfirmDeleteHeadlineRequested(
133+
_ConfirmDeleteHeadlineRequested event,
134+
Emitter<ArchivedHeadlinesState> emit,
135+
) async {
106136
try {
107137
await _headlinesRepository.delete(id: event.id);
138+
emit(state.copyWith(lastDeletedHeadline: null));
108139
} on HttpException catch (e) {
109-
emit(state.copyWith(headlines: originalHeadlines, exception: e));
140+
// If deletion fails, restore the headline to the list
141+
final originalHeadlines = List<Headline>.from(state.headlines)
142+
..add(state.lastDeletedHeadline!);
143+
emit(
144+
state.copyWith(
145+
headlines: originalHeadlines,
146+
exception: e,
147+
lastDeletedHeadline: null,
148+
),
149+
);
110150
} catch (e) {
151+
final originalHeadlines = List<Headline>.from(state.headlines)
152+
..add(state.lastDeletedHeadline!);
111153
emit(
112154
state.copyWith(
113155
headlines: originalHeadlines,
114156
exception: UnknownException('An unexpected error occurred: $e'),
157+
lastDeletedHeadline: null,
158+
),
159+
);
160+
}
161+
}
162+
163+
void _onUndoDeleteHeadlineRequested(
164+
UndoDeleteHeadlineRequested event,
165+
Emitter<ArchivedHeadlinesState> emit,
166+
) {
167+
_deleteTimer?.cancel();
168+
if (state.lastDeletedHeadline != null) {
169+
final updatedHeadlines = List<Headline>.from(state.headlines)
170+
..insert(
171+
state.headlines.indexWhere(
172+
(h) =>
173+
h.updatedAt.isBefore(state.lastDeletedHeadline!.updatedAt),
174+
) !=
175+
-1
176+
? state.headlines.indexWhere(
177+
(h) =>
178+
h.updatedAt.isBefore(state.lastDeletedHeadline!.updatedAt),
179+
)
180+
: state.headlines.length,
181+
state.lastDeletedHeadline!,
182+
);
183+
emit(
184+
state.copyWith(
185+
headlines: updatedHeadlines,
186+
lastDeletedHeadline: null,
115187
),
116188
);
117189
}

lib/content_management/bloc/archived_headlines/archived_headlines_event.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,18 @@ final class DeleteHeadlineForeverRequested extends ArchivedHeadlinesEvent {
3737
@override
3838
List<Object?> get props => [id];
3939
}
40+
41+
/// Event to undo the deletion of a headline.
42+
final class UndoDeleteHeadlineRequested extends ArchivedHeadlinesEvent {
43+
const UndoDeleteHeadlineRequested();
44+
}
45+
46+
/// Internal event to confirm the permanent deletion of a headline after a delay.
47+
final class _ConfirmDeleteHeadlineRequested extends ArchivedHeadlinesEvent {
48+
const _ConfirmDeleteHeadlineRequested(this.id);
49+
50+
final String id;
51+
52+
@override
53+
List<Object?> get props => [id];
54+
}

lib/content_management/bloc/archived_headlines/archived_headlines_state.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ArchivedHeadlinesState extends Equatable {
1717
this.hasMore = false,
1818
this.exception,
1919
this.restoredHeadline,
20+
this.lastDeletedHeadline,
2021
});
2122

2223
final ArchivedHeadlinesStatus status;
@@ -25,6 +26,7 @@ class ArchivedHeadlinesState extends Equatable {
2526
final bool hasMore;
2627
final HttpException? exception;
2728
final Headline? restoredHeadline;
29+
final Headline? lastDeletedHeadline;
2830

2931
ArchivedHeadlinesState copyWith({
3032
ArchivedHeadlinesStatus? status,
@@ -33,14 +35,16 @@ class ArchivedHeadlinesState extends Equatable {
3335
bool? hasMore,
3436
HttpException? exception,
3537
Headline? restoredHeadline,
38+
Headline? lastDeletedHeadline,
3639
}) {
3740
return ArchivedHeadlinesState(
3841
status: status ?? this.status,
3942
headlines: headlines ?? this.headlines,
4043
cursor: cursor ?? this.cursor,
4144
hasMore: hasMore ?? this.hasMore,
42-
exception: exception ?? this.exception,
45+
exception: exception,
4346
restoredHeadline: restoredHeadline,
47+
lastDeletedHeadline: lastDeletedHeadline,
4448
);
4549
}
4650

@@ -52,5 +56,6 @@ class ArchivedHeadlinesState extends Equatable {
5256
hasMore,
5357
exception,
5458
restoredHeadline,
59+
lastDeletedHeadline,
5560
];
5661
}

lib/content_management/view/archived_headlines_page.dart

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/content_manageme
77
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/content_management_bloc.dart';
88
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart';
99
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart';
10+
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart';
1011
import 'package:intl/intl.dart';
1112
import 'package:ui_kit/ui_kit.dart';
1213

@@ -38,15 +39,35 @@ class _ArchivedHeadlinesView extends StatelessWidget {
3839
padding: const EdgeInsets.all(AppSpacing.lg),
3940
child: BlocListener<ArchivedHeadlinesBloc, ArchivedHeadlinesState>(
4041
listenWhen: (previous, current) =>
41-
previous.status != current.status ||
42+
previous.lastDeletedHeadline != current.lastDeletedHeadline ||
4243
previous.restoredHeadline != current.restoredHeadline,
4344
listener: (context, state) {
44-
if (state.status == ArchivedHeadlinesStatus.success &&
45-
state.restoredHeadline != null) {
45+
if (state.restoredHeadline != null) {
4646
context.read<ContentManagementBloc>().add(
4747
const LoadHeadlinesRequested(limit: kDefaultRowsPerPage),
4848
);
4949
}
50+
if (state.lastDeletedHeadline != null) {
51+
final truncatedTitle =
52+
state.lastDeletedHeadline!.title.truncate(30);
53+
ScaffoldMessenger.of(context)
54+
..hideCurrentSnackBar()
55+
..showSnackBar(
56+
SnackBar(
57+
content: Text(
58+
l10n.headlineDeleted(truncatedTitle),
59+
),
60+
action: SnackBarAction(
61+
label: l10n.undo,
62+
onPressed: () {
63+
context
64+
.read<ArchivedHeadlinesBloc>()
65+
.add(const UndoDeleteHeadlineRequested());
66+
},
67+
),
68+
),
69+
);
70+
}
5071
},
5172
child: BlocBuilder<ArchivedHeadlinesBloc, ArchivedHeadlinesState>(
5273
builder: (context, state) {

lib/l10n/app_localizations.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,6 +1753,18 @@ abstract class AppLocalizations {
17531753
/// In en, this message translates to:
17541754
/// **'Archive'**
17551755
String get archive;
1756+
1757+
/// Snackbar message when a headline is deleted
1758+
///
1759+
/// In en, this message translates to:
1760+
/// **'Deleted \'\'{title}\'\'.'**
1761+
String headlineDeleted(String title);
1762+
1763+
/// No description provided for @undo.
1764+
///
1765+
/// In en, this message translates to:
1766+
/// **'Undo'**
1767+
String get undo;
17561768
}
17571769

17581770
class _AppLocalizationsDelegate

lib/l10n/app_localizations_ar.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,4 +922,12 @@ class AppLocalizationsAr extends AppLocalizations {
922922

923923
@override
924924
String get archive => 'أرشفة';
925+
926+
@override
927+
String headlineDeleted(String title) {
928+
return 'تم حذف \'\'$title\'\'.';
929+
}
930+
931+
@override
932+
String get undo => 'تراجع';
925933
}

lib/l10n/app_localizations_en.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,4 +921,12 @@ class AppLocalizationsEn extends AppLocalizations {
921921

922922
@override
923923
String get archive => 'Archive';
924+
925+
@override
926+
String headlineDeleted(String title) {
927+
return 'Deleted \'\'$title\'\'.';
928+
}
929+
930+
@override
931+
String get undo => 'Undo';
924932
}

lib/l10n/arb/app_ar.arb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,17 @@
11481148
},
11491149
"archive": "أرشفة",
11501150
"@archive": {
1151-
"description": "تلميح لزر الأرشفة"
1152-
}
1151+
"description": "تلميح لأرشفة زر"
1152+
},
1153+
"headlineDeleted": "تم حذف ''{title}''.",
1154+
"@headlineDeleted": {
1155+
"description": "رسالة Snackbar عند حذف عنوان",
1156+
"placeholders": {
1157+
"title": {
1158+
"type": "String",
1159+
"example": "عاجل"
1160+
}
1161+
}
1162+
},
1163+
"undo": "تراجع"
11531164
}

lib/l10n/arb/app_en.arb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,8 +1080,7 @@
10801080
"feedActionTypeEnableNotifications": "Enable Notifications",
10811081
"@feedActionTypeEnableNotifications": {
10821082
"description": "Feed action type for enabling notifications"
1083-
}
1084-
,
1083+
},
10851084
"countryPickerSearchLabel": "Search",
10861085
"@countryPickerSearchLabel": {
10871086
"description": "Label for the search input in the country picker"
@@ -1149,5 +1148,16 @@
11491148
"archive": "Archive",
11501149
"@archive": {
11511150
"description": "Tooltip for the archive button"
1152-
}
1151+
},
1152+
"headlineDeleted": "Deleted ''{title}''.",
1153+
"@headlineDeleted": {
1154+
"description": "Snackbar message when a headline is deleted",
1155+
"placeholders": {
1156+
"title": {
1157+
"type": "String",
1158+
"example": "Breaking News"
1159+
}
1160+
}
1161+
},
1162+
"undo": "Undo"
11531163
}

lib/shared/extensions/extensions.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export 'content_status_l10n.dart';
2+
export 'source_type_l10n.dart';
3+
export 'string_truncate.dart';

0 commit comments

Comments
 (0)