Skip to content

Commit f1bddb4

Browse files
authored
Merge pull request #141 from flutter-news-app-full-source-code/refactor/enhance-headlines-table-ui
Refactor/enhance headlines table UI
2 parents c5656f7 + d21bebe commit f1bddb4

18 files changed

+699
-191
lines changed

lib/content_management/bloc/content_management_bloc.dart

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'package:equatable/equatable.dart';
77
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/headlines_filter/headlines_filter_bloc.dart';
88
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/sources_filter/sources_filter_bloc.dart';
99
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/topics_filter/topics_filter_bloc.dart';
10-
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/models/breaking_news_filter_status.dart';
1110
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/constants/app_constants.dart';
1211
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart';
1312
import 'package:ui_kit/ui_kit.dart';
@@ -154,15 +153,9 @@ class ContentManagementBloc
154153
filter['eventCountry.id'] = {r'$in': state.selectedCountryIds};
155154
}
156155

157-
// Handle the breaking news filter based on the enum status.
158-
switch (state.isBreaking) {
159-
case BreakingNewsFilterStatus.breakingOnly:
160-
filter['isBreaking'] = true;
161-
case BreakingNewsFilterStatus.nonBreakingOnly:
162-
filter['isBreaking'] = false;
163-
case BreakingNewsFilterStatus.all:
164-
// For 'all', we don't add the 'isBreaking' key to the filter.
165-
break;
156+
// If the breaking news filter is active, add it to the query.
157+
if (state.isBreaking) {
158+
filter['isBreaking'] = true;
166159
}
167160

168161
return filter;
@@ -394,14 +387,15 @@ class ContentManagementBloc
394387
state.copyWith(
395388
headlines: updatedHeadlines,
396389
lastPendingDeletionId: event.id,
397-
snackbarMessage: 'Headline "${headlineToDelete.title}" deleted.',
390+
itemPendingDeletion: headlineToDelete,
398391
),
399392
);
400393

401394
_pendingDeletionsService.requestDeletion(
402395
item: headlineToDelete,
403396
repository: _headlinesRepository,
404397
undoDuration: AppConstants.kSnackbarDuration,
398+
// messageBuilder is omitted, UI will build the message
405399
);
406400
}
407401

@@ -585,14 +579,15 @@ class ContentManagementBloc
585579
state.copyWith(
586580
topics: updatedTopics,
587581
lastPendingDeletionId: event.id,
588-
snackbarMessage: 'Topic "${topicToDelete.name}" deleted.',
582+
itemPendingDeletion: topicToDelete,
589583
),
590584
);
591585

592586
_pendingDeletionsService.requestDeletion(
593587
item: topicToDelete,
594588
repository: _topicsRepository,
595589
undoDuration: AppConstants.kSnackbarDuration,
590+
// messageBuilder is omitted, UI will build the message
596591
);
597592
}
598593

@@ -776,14 +771,15 @@ class ContentManagementBloc
776771
state.copyWith(
777772
sources: updatedSources,
778773
lastPendingDeletionId: event.id,
779-
snackbarMessage: 'Source "${sourceToDelete.name}" deleted.',
774+
itemPendingDeletion: sourceToDelete,
780775
),
781776
);
782777

783778
_pendingDeletionsService.requestDeletion(
784779
item: sourceToDelete,
785780
repository: _sourcesRepository,
786781
undoDuration: AppConstants.kSnackbarDuration,
782+
// messageBuilder is omitted, UI will build the message
787783
);
788784
}
789785

@@ -804,13 +800,20 @@ class ContentManagementBloc
804800
Emitter<ContentManagementState> emit,
805801
) async {
806802
switch (event.event.status) {
803+
case DeletionStatus.requested:
804+
// This case is now handled by the optimistic UI update in the
805+
// specific delete handlers (e.g., _onDeleteHeadlineForeverRequested).
806+
// The itemPendingDeletion is set there, which the UI uses to build
807+
// the snackbar message.
808+
break;
807809
case DeletionStatus.confirmed:
808810
// If deletion is confirmed, clear pending status.
809811
// The item was already optimistically removed from the list.
810812
emit(
811813
state.copyWith(
812-
lastPendingDeletionId: null,
813-
snackbarMessage: null,
814+
lastPendingDeletionId: null, // Clear the pending ID
815+
// Clear the item so the snackbar doesn't reappear on rebuilds
816+
itemPendingDeletion: null,
814817
),
815818
);
816819
case DeletionStatus.undone:
@@ -823,8 +826,8 @@ class ContentManagementBloc
823826
emit(
824827
state.copyWith(
825828
headlines: updatedHeadlines,
826-
lastPendingDeletionId: null,
827-
snackbarMessage: null,
829+
lastPendingDeletionId: null, // Clear the pending ID
830+
itemPendingDeletion: null,
828831
),
829832
);
830833
} else if (item is Topic) {
@@ -834,8 +837,8 @@ class ContentManagementBloc
834837
emit(
835838
state.copyWith(
836839
topics: updatedTopics,
837-
lastPendingDeletionId: null,
838-
snackbarMessage: null,
840+
lastPendingDeletionId: null, // Clear the pending ID
841+
itemPendingDeletion: null,
839842
),
840843
);
841844
} else if (item is Source) {
@@ -845,8 +848,8 @@ class ContentManagementBloc
845848
emit(
846849
state.copyWith(
847850
sources: updatedSources,
848-
lastPendingDeletionId: null,
849-
snackbarMessage: null,
851+
lastPendingDeletionId: null, // Clear the pending ID
852+
itemPendingDeletion: null,
850853
),
851854
);
852855
}

lib/content_management/bloc/content_management_state.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class ContentManagementState extends Equatable {
3636
this.sourcesHasMore = false,
3737
this.exception,
3838
this.lastPendingDeletionId,
39-
this.snackbarMessage,
39+
this.itemPendingDeletion,
4040
});
4141

4242
/// The currently active tab in the content management section.
@@ -85,9 +85,9 @@ class ContentManagementState extends Equatable {
8585
/// Used to trigger the snackbar display.
8686
final String? lastPendingDeletionId;
8787

88-
/// The message to display in the snackbar for pending deletions or other
89-
/// transient messages.
90-
final String? snackbarMessage;
88+
/// The item that was just requested for deletion, used by the UI to show
89+
/// a confirmation snackbar.
90+
final FeedItem? itemPendingDeletion;
9191

9292
/// Creates a copy of this [ContentManagementState] with updated values.
9393
ContentManagementState copyWith({
@@ -106,7 +106,7 @@ class ContentManagementState extends Equatable {
106106
bool? sourcesHasMore,
107107
HttpException? exception,
108108
String? lastPendingDeletionId,
109-
String? snackbarMessage,
109+
FeedItem? itemPendingDeletion,
110110
}) {
111111
return ContentManagementState(
112112
activeTab: activeTab ?? this.activeTab,
@@ -124,7 +124,7 @@ class ContentManagementState extends Equatable {
124124
sourcesHasMore: sourcesHasMore ?? this.sourcesHasMore,
125125
exception: exception,
126126
lastPendingDeletionId: lastPendingDeletionId,
127-
snackbarMessage: snackbarMessage,
127+
itemPendingDeletion: itemPendingDeletion,
128128
);
129129
}
130130

@@ -145,6 +145,6 @@ class ContentManagementState extends Equatable {
145145
sourcesHasMore,
146146
exception,
147147
lastPendingDeletionId,
148-
snackbarMessage,
148+
itemPendingDeletion,
149149
];
150150
}

lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'package:bloc/bloc.dart';
22
import 'package:core/core.dart';
33
import 'package:equatable/equatable.dart';
4-
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/models/breaking_news_filter_status.dart';
54

65
part 'headlines_filter_event.dart';
76
part 'headlines_filter_state.dart';
@@ -79,8 +78,7 @@ class HeadlinesFilterBloc
7978

8079
/// Handles changes to the breaking news filter.
8180
///
82-
/// This updates the `isBreaking` status for the filter using the
83-
/// [BreakingNewsFilterStatus] enum.
81+
/// This updates the `isBreaking` status for the filter.
8482
void _onHeadlinesBreakingNewsFilterChanged(
8583
HeadlinesBreakingNewsFilterChanged event,
8684
Emitter<HeadlinesFilterState> emit,
@@ -139,9 +137,8 @@ class HeadlinesFilterBloc
139137
if (state.selectedCountryIds.isNotEmpty) {
140138
filter['eventCountry.id'] = {r'$in': state.selectedCountryIds};
141139
}
142-
if (state.isBreaking != BreakingNewsFilterStatus.all) {
143-
filter['isBreaking'] =
144-
state.isBreaking == BreakingNewsFilterStatus.breakingOnly;
140+
if (state.isBreaking) {
141+
filter['isBreaking'] = true;
145142
}
146143

147144
return filter;

lib/content_management/bloc/headlines_filter/headlines_filter_event.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ final class HeadlinesCountryFilterChanged extends HeadlinesFilterEvent {
6262
final class HeadlinesBreakingNewsFilterChanged extends HeadlinesFilterEvent {
6363
const HeadlinesBreakingNewsFilterChanged(this.isBreaking);
6464

65-
final BreakingNewsFilterStatus isBreaking;
65+
final bool isBreaking;
6666

6767
@override
68-
List<Object?> get props => [isBreaking];
68+
List<Object> get props => [isBreaking];
6969
}
7070

7171
/// Event to request applying all current filters.
@@ -84,7 +84,7 @@ final class HeadlinesFilterApplied extends HeadlinesFilterEvent {
8484
final List<String> selectedSourceIds;
8585
final List<String> selectedTopicIds;
8686
final List<String> selectedCountryIds;
87-
final BreakingNewsFilterStatus isBreaking;
87+
final bool isBreaking;
8888

8989
@override
9090
List<Object?> get props => [

lib/content_management/bloc/headlines_filter/headlines_filter_state.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class HeadlinesFilterState extends Equatable {
1515
this.selectedSourceIds = const [],
1616
this.selectedTopicIds = const [],
1717
this.selectedCountryIds = const [],
18-
this.isBreaking = BreakingNewsFilterStatus.all,
18+
this.isBreaking = false,
1919
});
2020

2121
/// The current text in the search query field.
@@ -33,9 +33,8 @@ class HeadlinesFilterState extends Equatable {
3333
/// The list of country IDs to be included in the filter.
3434
final List<String> selectedCountryIds;
3535

36-
/// The breaking news status to filter by.
37-
/// `null` = all, `true` = breaking only, `false` = non-breaking only.
38-
final BreakingNewsFilterStatus isBreaking;
36+
/// A flag to filter for breaking news only.
37+
final bool isBreaking;
3938

4039
/// Creates a copy of this state with the given fields replaced with the
4140
/// new values.
@@ -45,7 +44,7 @@ class HeadlinesFilterState extends Equatable {
4544
List<String>? selectedSourceIds,
4645
List<String>? selectedTopicIds,
4746
List<String>? selectedCountryIds,
48-
BreakingNewsFilterStatus? isBreaking,
47+
bool? isBreaking,
4948
}) {
5049
return HeadlinesFilterState(
5150
searchQuery: searchQuery ?? this.searchQuery,

lib/content_management/view/content_management_page.dart

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,27 @@ class _ContentManagementPageState extends State<ContentManagementPage>
135135
),
136136
BlocListener<ContentManagementBloc, ContentManagementState>(
137137
listenWhen: (previous, current) =>
138-
previous.snackbarMessage != current.snackbarMessage &&
139-
current.snackbarMessage != null,
138+
previous.itemPendingDeletion != current.itemPendingDeletion &&
139+
current.itemPendingDeletion != null,
140140
listener: (context, state) {
141+
final item = state.itemPendingDeletion!;
142+
String itemType;
143+
String itemName;
144+
if (item is Headline) {
145+
itemType = l10n.headline;
146+
itemName = item.title;
147+
} else if (item is Topic) {
148+
itemType = l10n.topic;
149+
itemName = item.name;
150+
} else {
151+
itemType = l10n.source;
152+
itemName = (item as Source).name;
153+
}
141154
ScaffoldMessenger.of(context)
142155
..hideCurrentSnackBar()
143156
..showSnackBar(
144157
SnackBar(
145-
content: Text(state.snackbarMessage!),
158+
content: Text(l10n.itemDeletedSnackbar(itemType, itemName)),
146159
action: SnackBarAction(
147160
label: l10n.undo,
148161
onPressed: () {

lib/content_management/view/headlines_page.dart

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -221,32 +221,30 @@ class _HeadlinesDataSource extends DataTableSource {
221221
},
222222
cells: [
223223
DataCell(
224-
Stack(
225-
alignment: AlignmentDirectional.centerStart,
226-
children: <Widget>[
227-
// Add padding to the text to make space for the icon only when
228-
// the headline is breaking news. This ensures all titles align
229-
// vertically regardless of the icon's presence.
230-
Padding(
231-
padding: EdgeInsetsDirectional.only(
232-
start: headline.isBreaking
233-
? AppSpacing.xl + AppSpacing.xs
234-
: 0,
235-
),
236-
child: Text(
237-
headline.title,
238-
maxLines: 2,
239-
overflow: TextOverflow.ellipsis,
240-
),
241-
),
242-
// Conditionally display the icon at the start of the cell.
243-
if (headline.isBreaking)
244-
Icon(
245-
Icons.flash_on,
246-
size: 18,
247-
color: Theme.of(context).colorScheme.primary,
248-
),
249-
],
224+
RichText(
225+
maxLines: 2,
226+
overflow: TextOverflow.ellipsis,
227+
text: TextSpan(
228+
style: Theme.of(context).textTheme.bodyMedium,
229+
children: [
230+
TextSpan(text: headline.title),
231+
if (headline.isBreaking)
232+
WidgetSpan(
233+
alignment: PlaceholderAlignment.middle,
234+
child: Padding(
235+
padding: const EdgeInsets.only(left: AppSpacing.xs),
236+
child: Tooltip(
237+
message: l10n.breakingNewsHint,
238+
child: Icon(
239+
Icons.flash_on,
240+
size: 14,
241+
color: Theme.of(context).colorScheme.primary,
242+
),
243+
),
244+
),
245+
),
246+
],
247+
),
250248
),
251249
),
252250
if (!isMobile) // Conditionally show Source Name

0 commit comments

Comments
 (0)