From 351a651d7f39dcfd0ceeaa70aea77758d9f7c3eb Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:21:49 +0100 Subject: [PATCH 01/48] refactor(content_management): rename deletion events to archiving - Rename DeleteHeadlineRequested to ArchiveHeadlineRequested - Rename DeleteTopicRequested to ArchiveTopicRequested - Rename DeleteSourceRequested to ArchiveSourceRequested - Update comments to reflect the change from deletion to archiving --- .../bloc/content_management_event.dart | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/content_management/bloc/content_management_event.dart b/lib/content_management/bloc/content_management_event.dart index e9fb3cad..3e1d95b2 100644 --- a/lib/content_management/bloc/content_management_event.dart +++ b/lib/content_management/bloc/content_management_event.dart @@ -38,14 +38,14 @@ final class LoadHeadlinesRequested extends ContentManagementEvent { List get props => [startAfterId, limit]; } -/// {@template delete_headline_requested} -/// Event to request deletion of a headline. +/// {@template archive_headline_requested} +/// Event to request archiving of a headline. /// {@endtemplate} -final class DeleteHeadlineRequested extends ContentManagementEvent { - /// {@macro delete_headline_requested} - const DeleteHeadlineRequested(this.id); +final class ArchiveHeadlineRequested extends ContentManagementEvent { + /// {@macro archive_headline_requested} + const ArchiveHeadlineRequested(this.id); - /// The ID of the headline to delete. + /// The ID of the headline to archive. final String id; @override @@ -83,14 +83,14 @@ final class LoadTopicsRequested extends ContentManagementEvent { List get props => [startAfterId, limit]; } -/// {@template delete_topic_requested} -/// Event to request deletion of a topic. +/// {@template archive_topic_requested} +/// Event to request archiving of a topic. /// {@endtemplate} -final class DeleteTopicRequested extends ContentManagementEvent { - /// {@macro delete_topic_requested} - const DeleteTopicRequested(this.id); +final class ArchiveTopicRequested extends ContentManagementEvent { + /// {@macro archive_topic_requested} + const ArchiveTopicRequested(this.id); - /// The ID of the topic to delete. + /// The ID of the topic to archive. final String id; @override @@ -128,14 +128,14 @@ final class LoadSourcesRequested extends ContentManagementEvent { List get props => [startAfterId, limit]; } -/// {@template delete_source_requested} -/// Event to request deletion of a source. +/// {@template archive_source_requested} +/// Event to request archiving of a source. /// {@endtemplate} -final class DeleteSourceRequested extends ContentManagementEvent { - /// {@macro delete_source_requested} - const DeleteSourceRequested(this.id); +final class ArchiveSourceRequested extends ContentManagementEvent { + /// {@macro archive_source_requested} + const ArchiveSourceRequested(this.id); - /// The ID of the source to delete. + /// The ID of the source to archive. final String id; @override From c3c0fb6d416d67c74307619de059f9c13edbbfc7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:22:28 +0100 Subject: [PATCH 02/48] feat(content_management): implement archive functionality for headlines, topics, and sources - Rename DeleteHeadlineRequested, DeleteTopicRequested, and DeleteSourceRequested events to ArchiveHeadlineRequested, ArchiveTopicRequested, and ArchiveSourceRequested respectively - Update event handlers to archive items instead of deleting them - Implement optimistic UI updates for archiving items - Revert UI changes if archive operation fails --- .../bloc/content_management_bloc.dart | 84 +++++++++++++------ 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index c47e7c2c..12a0c5e2 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -31,13 +31,13 @@ class ContentManagementBloc on(_onContentManagementTabChanged); on(_onLoadHeadlinesRequested); on(_onHeadlineUpdated); - on(_onDeleteHeadlineRequested); + on(_onArchiveHeadlineRequested); on(_onLoadTopicsRequested); on(_onTopicUpdated); - on(_onDeleteTopicRequested); + on(_onArchiveTopicRequested); on(_onLoadSourcesRequested); on(_onSourceUpdated); - on(_onDeleteSourceRequested); + on(_onArchiveSourceRequested); } final DataRepository _headlinesRepository; @@ -92,17 +92,29 @@ class ContentManagementBloc } } - Future _onDeleteHeadlineRequested( - DeleteHeadlineRequested event, + Future _onArchiveHeadlineRequested( + ArchiveHeadlineRequested event, Emitter emit, ) async { + // Optimistically remove the headline from the list + final originalHeadlines = List.from(state.headlines); + final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id); + if (headlineIndex == -1) return; // Headline not found + + final headlineToArchive = originalHeadlines[headlineIndex]; + final updatedHeadlines = originalHeadlines..removeAt(headlineIndex); + + emit(state.copyWith(headlines: updatedHeadlines)); + try { - await _headlinesRepository.delete(id: event.id); - final updatedHeadlines = state.headlines - .where((h) => h.id != event.id) - .toList(); - emit(state.copyWith(headlines: updatedHeadlines)); + await _headlinesRepository.update( + id: event.id, + item: headlineToArchive.copyWith(status: ContentStatus.archived), + ); } on HttpException catch (e) { + // If the update fails, revert the change in the UI + emit(state.copyWith(headlines: originalHeadlines)); + // And then show the error emit( state.copyWith( headlinesStatus: ContentManagementStatus.failure, @@ -172,17 +184,29 @@ class ContentManagementBloc } } - Future _onDeleteTopicRequested( - DeleteTopicRequested event, + Future _onArchiveTopicRequested( + ArchiveTopicRequested event, Emitter emit, ) async { + // Optimistically remove the topic from the list + final originalTopics = List.from(state.topics); + final topicIndex = originalTopics.indexWhere((t) => t.id == event.id); + if (topicIndex == -1) return; // Topic not found + + final topicToArchive = originalTopics[topicIndex]; + final updatedTopics = originalTopics..removeAt(topicIndex); + + emit(state.copyWith(topics: updatedTopics)); + try { - await _topicsRepository.delete(id: event.id); - final updatedTopics = state.topics - .where((c) => c.id != event.id) - .toList(); - emit(state.copyWith(topics: updatedTopics)); + await _topicsRepository.update( + id: event.id, + item: topicToArchive.copyWith(status: ContentStatus.archived), + ); } on HttpException catch (e) { + // If the update fails, revert the change in the UI + emit(state.copyWith(topics: originalTopics)); + // And then show the error emit( state.copyWith( topicsStatus: ContentManagementStatus.failure, @@ -252,17 +276,29 @@ class ContentManagementBloc } } - Future _onDeleteSourceRequested( - DeleteSourceRequested event, + Future _onArchiveSourceRequested( + ArchiveSourceRequested event, Emitter emit, ) async { + // Optimistically remove the source from the list + final originalSources = List.from(state.sources); + final sourceIndex = originalSources.indexWhere((s) => s.id == event.id); + if (sourceIndex == -1) return; // Source not found + + final sourceToArchive = originalSources[sourceIndex]; + final updatedSources = originalSources..removeAt(sourceIndex); + + emit(state.copyWith(sources: updatedSources)); + try { - await _sourcesRepository.delete(id: event.id); - final updatedSources = state.sources - .where((s) => s.id != event.id) - .toList(); - emit(state.copyWith(sources: updatedSources)); + await _sourcesRepository.update( + id: event.id, + item: sourceToArchive.copyWith(status: ContentStatus.archived), + ); } on HttpException catch (e) { + // If the update fails, revert the change in the UI + emit(state.copyWith(sources: originalSources)); + // And then show the error emit( state.copyWith( sourcesStatus: ContentManagementStatus.failure, From 1013dc5e787013adc621526236b5b3bdfc9efdcf Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:22:41 +0100 Subject: [PATCH 03/48] feat(content_management): replace delete functionality with archive - Change delete icon to archive icon in headlines page - Update tooltip to reflect archive action - Dispatch ArchiveHeadlineRequested event instead of DeleteHeadlineRequested --- lib/content_management/view/headlines_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index 8555d388..fd1f1558 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -174,11 +174,11 @@ class _HeadlinesDataSource extends DataTableSource { }, ), IconButton( - icon: const Icon(Icons.delete), + icon: const Icon(Icons.archive), + tooltip: l10n.archive, onPressed: () { - // Dispatch delete event context.read().add( - DeleteHeadlineRequested(headline.id), + ArchiveHeadlineRequested(headline.id), ); }, ), From e76dc716b569c5f35f9e65c736deb62be6cb1159 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:28:22 +0100 Subject: [PATCH 04/48] refactor(content_management): replace delete functionality with archive - Change icon from delete to archive - Update tooltip to 'Archive' (temporary solution) - Replace DeleteSourceRequested event with ArchiveSourceRequested event This change improves the user experience by implementing a reversible action instead of permanent deletion. --- lib/content_management/view/sources_page.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index 1e608400..bbc6140e 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -173,11 +173,12 @@ class _SourcesDataSource extends DataTableSource { }, ), IconButton( - icon: const Icon(Icons.delete), + icon: const Icon(Icons.archive), + tooltip: 'Archive', // TODO(you): Will be fixed in l10n phase. onPressed: () { // Dispatch delete event context.read().add( - DeleteSourceRequested(source.id), + ArchiveSourceRequested(source.id), ); }, ), From 632be8fbf1358663114230d7e60379856e7cc398 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:28:33 +0100 Subject: [PATCH 05/48] refactor(content_management): replace delete action with archive - Replaced delete icon with archive icon - Updated tooltip to 'Archive' (to be localized in future) - Changed event from DeleteTopicRequested to ArchiveTopicRequested --- lib/content_management/view/topics_page.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/content_management/view/topics_page.dart b/lib/content_management/view/topics_page.dart index af06e00e..18204a0f 100644 --- a/lib/content_management/view/topics_page.dart +++ b/lib/content_management/view/topics_page.dart @@ -171,11 +171,12 @@ class _TopicsDataSource extends DataTableSource { }, ), IconButton( - icon: const Icon(Icons.delete), + icon: const Icon(Icons.archive), + tooltip: 'Archive', // todo(you): Will be fixed in l10n phase. onPressed: () { // Dispatch delete event context.read().add( - DeleteTopicRequested(topic.id), + ArchiveTopicRequested(topic.id), ); }, ), From 7c1dd744d2f919427c04976704407d5a45364c1a Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:30:13 +0100 Subject: [PATCH 06/48] fix(content_management): allow text overflow for headline title - Wrap headline title Text widget in DataCell with maxLines set to 2 - Add TextOverflow.ellipsis to handle overflow - This change prevents layout issues with long headline titles --- lib/content_management/view/headlines_page.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index fd1f1558..3ba7738f 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -151,7 +151,13 @@ class _HeadlinesDataSource extends DataTableSource { } }, cells: [ - DataCell(Text(headline.title)), + DataCell( + Text( + headline.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), DataCell(Text(headline.source.name)), DataCell(Text(headline.status.l10n(context))), DataCell( From f8dd350a83ef152905dc0a0a6494a6518b559aa4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:31:37 +0100 Subject: [PATCH 07/48] refactor(content_management): improve topics page UI and code consistency - Make topic name text ellipsis for long text - Adjust TODO comment format --- lib/content_management/view/topics_page.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/content_management/view/topics_page.dart b/lib/content_management/view/topics_page.dart index 18204a0f..49752616 100644 --- a/lib/content_management/view/topics_page.dart +++ b/lib/content_management/view/topics_page.dart @@ -149,7 +149,13 @@ class _TopicsDataSource extends DataTableSource { } }, cells: [ - DataCell(Text(topic.name)), + DataCell( + Text( + topic.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), DataCell(Text(topic.status.l10n(context))), DataCell( Text( @@ -172,7 +178,7 @@ class _TopicsDataSource extends DataTableSource { ), IconButton( icon: const Icon(Icons.archive), - tooltip: 'Archive', // todo(you): Will be fixed in l10n phase. + tooltip: 'Archive', // TODO(you): Will be fixed in l10n phase. onPressed: () { // Dispatch delete event context.read().add( From 08af2edc7c5dc7fbdcb6ff5a14ab56b92f66e964 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:31:53 +0100 Subject: [PATCH 08/48] fix(content_management): allow source name to overflow with an ellipsis - Modify DataCell to support multiline text with ellipsis overflow - Improve readability for long source names in the sources table --- lib/content_management/view/sources_page.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index bbc6140e..2267c2bb 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -150,7 +150,13 @@ class _SourcesDataSource extends DataTableSource { } }, cells: [ - DataCell(Text(source.name)), + DataCell( + Text( + source.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), DataCell(Text(source.sourceType.localizedName(l10n))), DataCell(Text(source.status.l10n(context))), DataCell( From 2853886e8a8232a7f1bdbaa82a8e6e8494c113f0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:41:00 +0100 Subject: [PATCH 09/48] fix(content_management): improve headline pagination and UI - Remove loading indicator row from _HeadlinesDataSource - Add LinearProgressIndicator for loading state - Adjust row count logic in _HeadlinesDataSource - Update tooltip text for archive button (TODO: to be fixed in l10n phase) - Refactor PaginatedDataTable2 widget for better layout --- .../view/headlines_page.dart | 135 +++++++++--------- 1 file changed, 65 insertions(+), 70 deletions(-) diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index 3ba7738f..a2a9e3b1 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -27,8 +27,8 @@ class _HeadlinesPageState extends State { void initState() { super.initState(); context.read().add( - const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), - ); + const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), + ); } @override @@ -51,8 +51,8 @@ class _HeadlinesPageState extends State { return FailureStateWidget( exception: state.exception!, onRetry: () => context.read().add( - const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), - ), + const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), + ), ); } @@ -60,49 +60,66 @@ class _HeadlinesPageState extends State { return Center(child: Text(l10n.noHeadlinesFound)); } - return PaginatedDataTable2( - columns: [ - DataColumn2(label: Text(l10n.headlineTitle), size: ColumnSize.L), - DataColumn2(label: Text(l10n.sourceName), size: ColumnSize.M), - DataColumn2(label: Text(l10n.status), size: ColumnSize.S), - DataColumn2(label: Text(l10n.lastUpdated), size: ColumnSize.M), - DataColumn2( - label: Text(l10n.actions), - size: ColumnSize.S, - fixedWidth: 120, + return Column( + children: [ + if (state.headlinesStatus == ContentManagementStatus.loading && + state.headlines.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.headlineTitle), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.sourceName), + size: ColumnSize.M, + ), + DataColumn2(label: Text(l10n.status), size: ColumnSize.S), + DataColumn2( + label: Text(l10n.lastUpdated), + size: ColumnSize.M, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + fixedWidth: 120, + ), + ], + source: _HeadlinesDataSource( + context: context, + headlines: state.headlines, + hasMore: state.headlinesHasMore, + l10n: l10n, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.headlines.length && + state.headlinesHasMore && + state.headlinesStatus != + ContentManagementStatus.loading) { + context.read().add( + LoadHeadlinesRequested( + startAfterId: state.headlinesCursor, + limit: kDefaultRowsPerPage, + ), + ); + } + }, + empty: Center(child: Text(l10n.noHeadlinesFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.md, + horizontalMargin: AppSpacing.md, + ), ), ], - source: _HeadlinesDataSource( - context: context, - headlines: state.headlines, - isLoading: - state.headlinesStatus == ContentManagementStatus.loading, - hasMore: state.headlinesHasMore, - l10n: l10n, - ), - rowsPerPage: kDefaultRowsPerPage, - availableRowsPerPage: const [kDefaultRowsPerPage], - onPageChanged: (pageIndex) { - final newOffset = pageIndex * kDefaultRowsPerPage; - if (newOffset >= state.headlines.length && - state.headlinesHasMore && - state.headlinesStatus != ContentManagementStatus.loading) { - context.read().add( - LoadHeadlinesRequested( - startAfterId: state.headlinesCursor, - limit: kDefaultRowsPerPage, - ), - ); - } - }, - empty: Center(child: Text(l10n.noHeadlinesFound)), - showCheckboxColumn: false, - showFirstLastButtons: true, - fit: FlexFit.tight, - headingRowHeight: 56, - dataRowHeight: 56, - columnSpacing: AppSpacing.md, - horizontalMargin: AppSpacing.md, ); }, ), @@ -114,30 +131,18 @@ class _HeadlinesDataSource extends DataTableSource { _HeadlinesDataSource({ required this.context, required this.headlines, - required this.isLoading, required this.hasMore, required this.l10n, }); final BuildContext context; final List headlines; - final bool isLoading; final bool hasMore; final AppLocalizations l10n; @override DataRow? getRow(int index) { if (index >= headlines.length) { - // This can happen if hasMore is true and the user is on the last page. - // If we are loading, show a spinner. Otherwise, we've reached the end. - if (isLoading) { - return DataRow2( - cells: List.generate( - 5, - (_) => const DataCell(Center(child: CircularProgressIndicator())), - ), - ); - } return null; } final headline = headlines[index]; @@ -181,11 +186,11 @@ class _HeadlinesDataSource extends DataTableSource { ), IconButton( icon: const Icon(Icons.archive), - tooltip: l10n.archive, + tooltip: 'Archive', // TODO(you): Will be fixed in l10n phase. onPressed: () { context.read().add( - ArchiveHeadlineRequested(headline.id), - ); + ArchiveHeadlineRequested(headline.id), + ); }, ), ], @@ -200,16 +205,6 @@ class _HeadlinesDataSource extends DataTableSource { @override int get rowCount { - // If we have more items to fetch, we add 1 to the current length. - // This signals to PaginatedDataTable2 that there is at least one more page, - // which enables the 'next page' button. - if (hasMore) { - // When loading, we show an extra row for the spinner. - // Otherwise, we just indicate that there are more rows. - return isLoading - ? headlines.length + 1 - : headlines.length + kDefaultRowsPerPage; - } return headlines.length; } From 2fcfb2183fd8189157b02390a0143da6bff23c45 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:41:51 +0100 Subject: [PATCH 10/48] fix(content_management): remove loading row from topics table - Remove loading row from PaginatedDataTable2 - Add LinearProgressIndicator for loading state - Adjust row count logic to remove extra row for loading state - Update TopicsDataSource to remove loading row handling --- lib/content_management/view/topics_page.dart | 126 +++++++++---------- 1 file changed, 59 insertions(+), 67 deletions(-) diff --git a/lib/content_management/view/topics_page.dart b/lib/content_management/view/topics_page.dart index 49752616..374d310b 100644 --- a/lib/content_management/view/topics_page.dart +++ b/lib/content_management/view/topics_page.dart @@ -27,8 +27,8 @@ class _TopicPageState extends State { void initState() { super.initState(); context.read().add( - const LoadTopicsRequested(limit: kDefaultRowsPerPage), - ); + const LoadTopicsRequested(limit: kDefaultRowsPerPage), + ); } @override @@ -51,8 +51,8 @@ class _TopicPageState extends State { return FailureStateWidget( exception: state.exception!, onRetry: () => context.read().add( - const LoadTopicsRequested(limit: kDefaultRowsPerPage), - ), + const LoadTopicsRequested(limit: kDefaultRowsPerPage), + ), ); } @@ -60,47 +60,61 @@ class _TopicPageState extends State { return Center(child: Text(l10n.noTopicsFound)); } - return PaginatedDataTable2( - columns: [ - DataColumn2(label: Text(l10n.topicName), size: ColumnSize.L), - DataColumn2(label: Text(l10n.status), size: ColumnSize.S), - DataColumn2(label: Text(l10n.lastUpdated), size: ColumnSize.M), - DataColumn2( - label: Text(l10n.actions), - size: ColumnSize.S, - fixedWidth: 120, + return Column( + children: [ + if (state.topicsStatus == ContentManagementStatus.loading && + state.topics.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.topicName), + size: ColumnSize.L, + ), + DataColumn2(label: Text(l10n.status), size: ColumnSize.S), + DataColumn2( + label: Text(l10n.lastUpdated), + size: ColumnSize.M, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + fixedWidth: 120, + ), + ], + source: _TopicsDataSource( + context: context, + topics: state.topics, + hasMore: state.topicsHasMore, + l10n: l10n, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.topics.length && + state.topicsHasMore && + state.topicsStatus != ContentManagementStatus.loading) { + context.read().add( + LoadTopicsRequested( + startAfterId: state.topicsCursor, + limit: kDefaultRowsPerPage, + ), + ); + } + }, + empty: Center(child: Text(l10n.noTopicsFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.md, + horizontalMargin: AppSpacing.md, + ), ), ], - source: _TopicsDataSource( - context: context, - topics: state.topics, - isLoading: state.topicsStatus == ContentManagementStatus.loading, - hasMore: state.topicsHasMore, - l10n: l10n, - ), - rowsPerPage: kDefaultRowsPerPage, - availableRowsPerPage: const [kDefaultRowsPerPage], - onPageChanged: (pageIndex) { - final newOffset = pageIndex * kDefaultRowsPerPage; - if (newOffset >= state.topics.length && - state.topicsHasMore && - state.topicsStatus != ContentManagementStatus.loading) { - context.read().add( - LoadTopicsRequested( - startAfterId: state.topicsCursor, - limit: kDefaultRowsPerPage, - ), - ); - } - }, - empty: Center(child: Text(l10n.noTopicsFound)), - showCheckboxColumn: false, - showFirstLastButtons: true, - fit: FlexFit.tight, - headingRowHeight: 56, - dataRowHeight: 56, - columnSpacing: AppSpacing.md, - horizontalMargin: AppSpacing.md, ); }, ), @@ -112,30 +126,18 @@ class _TopicsDataSource extends DataTableSource { _TopicsDataSource({ required this.context, required this.topics, - required this.isLoading, required this.hasMore, required this.l10n, }); final BuildContext context; final List topics; - final bool isLoading; final bool hasMore; final AppLocalizations l10n; @override DataRow? getRow(int index) { if (index >= topics.length) { - // This can happen if hasMore is true and the user is on the last page. - // If we are loading, show a spinner. Otherwise, we've reached the end. - if (isLoading) { - return DataRow2( - cells: List.generate( - 4, - (_) => const DataCell(Center(child: CircularProgressIndicator())), - ), - ); - } return null; } final topic = topics[index]; @@ -182,8 +184,8 @@ class _TopicsDataSource extends DataTableSource { onPressed: () { // Dispatch delete event context.read().add( - ArchiveTopicRequested(topic.id), - ); + ArchiveTopicRequested(topic.id), + ); }, ), ], @@ -198,16 +200,6 @@ class _TopicsDataSource extends DataTableSource { @override int get rowCount { - // If we have more items to fetch, we add 1 to the current length. - // This signals to PaginatedDataTable2 that there is at least one more page, - // which enables the 'next page' button. - if (hasMore) { - // When loading, we show an extra row for the spinner. - // Otherwise, we just indicate that there are more rows. - return isLoading - ? topics.length + 1 - : topics.length + kDefaultRowsPerPage; - } return topics.length; } From c8008806777c038e358fb0b92b7aa177803e74c7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:42:25 +0100 Subject: [PATCH 11/48] refactor(content_management): improve sources table loading state - Remove loading row from _SourcesDataSource - Add LinearProgressIndicator to _SourcesPageState - Adjust row count logic in _SourcesDataSource - Remove isLoading parameter from _SourcesDataSource constructor --- lib/content_management/view/sources_page.dart | 123 +++++++++--------- 1 file changed, 60 insertions(+), 63 deletions(-) diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index 2267c2bb..a78af66a 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -61,48 +61,66 @@ class _SourcesPageState extends State { return Center(child: Text(l10n.noSourcesFound)); } - return PaginatedDataTable2( - columns: [ - DataColumn2(label: Text(l10n.sourceName), size: ColumnSize.L), - DataColumn2(label: Text(l10n.sourceType), size: ColumnSize.M), - DataColumn2(label: Text(l10n.status), size: ColumnSize.S), - DataColumn2(label: Text(l10n.lastUpdated), size: ColumnSize.M), - DataColumn2( - label: Text(l10n.actions), - size: ColumnSize.S, - fixedWidth: 120, + return Column( + children: [ + if (state.sourcesStatus == ContentManagementStatus.loading && + state.sources.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.sourceName), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.sourceType), + size: ColumnSize.M, + ), + DataColumn2(label: Text(l10n.status), size: ColumnSize.S), + DataColumn2( + label: Text(l10n.lastUpdated), + size: ColumnSize.M, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + fixedWidth: 120, + ), + ], + source: _SourcesDataSource( + context: context, + sources: state.sources, + hasMore: state.sourcesHasMore, + l10n: l10n, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.sources.length && + state.sourcesHasMore && + state.sourcesStatus != + ContentManagementStatus.loading) { + context.read().add( + LoadSourcesRequested( + startAfterId: state.sourcesCursor, + limit: kDefaultRowsPerPage, + ), + ); + } + }, + empty: Center(child: Text(l10n.noSourcesFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.md, + horizontalMargin: AppSpacing.md, + ), ), ], - source: _SourcesDataSource( - context: context, - sources: state.sources, - isLoading: state.sourcesStatus == ContentManagementStatus.loading, - hasMore: state.sourcesHasMore, - l10n: l10n, - ), - rowsPerPage: kDefaultRowsPerPage, - availableRowsPerPage: const [kDefaultRowsPerPage], - onPageChanged: (pageIndex) { - final newOffset = pageIndex * kDefaultRowsPerPage; - if (newOffset >= state.sources.length && - state.sourcesHasMore && - state.sourcesStatus != ContentManagementStatus.loading) { - context.read().add( - LoadSourcesRequested( - startAfterId: state.sourcesCursor, - limit: kDefaultRowsPerPage, - ), - ); - } - }, - empty: Center(child: Text(l10n.noSourcesFound)), - showCheckboxColumn: false, - showFirstLastButtons: true, - fit: FlexFit.tight, - headingRowHeight: 56, - dataRowHeight: 56, - columnSpacing: AppSpacing.md, - horizontalMargin: AppSpacing.md, ); }, ), @@ -114,29 +132,18 @@ class _SourcesDataSource extends DataTableSource { _SourcesDataSource({ required this.context, required this.sources, - required this.isLoading, required this.hasMore, required this.l10n, }); final BuildContext context; final List sources; - final bool isLoading; final bool hasMore; final AppLocalizations l10n; @override DataRow? getRow(int index) { if (index >= sources.length) { - // This can happen if hasMore is true and the user is on the last page. - // If we are loading, show a spinner. Otherwise, we've reached the end. - if (isLoading) { - return DataRow2( - cells: List.generate(5, (_) { - return const DataCell(Center(child: CircularProgressIndicator())); - }), - ); - } return null; } final source = sources[index]; @@ -184,8 +191,8 @@ class _SourcesDataSource extends DataTableSource { onPressed: () { // Dispatch delete event context.read().add( - ArchiveSourceRequested(source.id), - ); + ArchiveSourceRequested(source.id), + ); }, ), ], @@ -200,16 +207,6 @@ class _SourcesDataSource extends DataTableSource { @override int get rowCount { - // If we have more items to fetch, we add 1 to the current length. - // This signals to PaginatedDataTable2 that there is at least one more page, - // which enables the 'next page' button. - if (hasMore) { - // When loading, we show an extra row for the spinner. - // Otherwise, we just indicate that there are more rows. - return isLoading - ? sources.length + 1 - : sources.length + kDefaultRowsPerPage; - } return sources.length; } From 0db0076f2f84f3bc78f7486861066b89bfb71b39 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:46:24 +0100 Subject: [PATCH 12/48] feat(router): add archived content page route - Import ArchivedContentPage in router.dart - Add new GoRoute for archived content page - Define routes for archived content in routes.dart --- lib/router/router.dart | 6 ++++++ lib/router/routes.dart | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/lib/router/router.dart b/lib/router/router.dart index 942e7f7b..ca684f33 100644 --- a/lib/router/router.dart +++ b/lib/router/router.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/view/edit_headline_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/view/edit_source_page.dart'; 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/view/archived_content_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/view/dashboard_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'; @@ -196,6 +197,11 @@ GoRouter createRouter({ return EditSourcePage(sourceId: id); }, ), + GoRoute( + path: Routes.archivedContent, + name: Routes.archivedContentName, + builder: (context, state) => const ArchivedContentPage(), + ), ], ), ], diff --git a/lib/router/routes.dart b/lib/router/routes.dart index 88346a03..66e7c190 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -44,6 +44,12 @@ abstract final class Routes { /// The name for the content management section route. static const String contentManagementName = 'contentManagement'; + /// The path for the archived content page. + static const String archivedContent = 'archived-content'; + + /// The name for the archived content page route. + static const String archivedContentName = 'archivedContent'; + /// The path for creating a new headline. static const String createHeadline = 'create-headline'; From a83aa113b4eff6b69c56838182668a153e23297d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:49:10 +0100 Subject: [PATCH 13/48] feat(content_management): add archived items button and update tooltips - Add IconButton for archived items in ContentManagementPage - Update tooltip for add new item button - Replace "Consider localizing" with "TODO(you): Will be fixed in l10n phase" for both tooltips --- lib/content_management/view/content_management_page.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/content_management_page.dart b/lib/content_management/view/content_management_page.dart index a5cb5803..ab8a1eb7 100644 --- a/lib/content_management/view/content_management_page.dart +++ b/lib/content_management/view/content_management_page.dart @@ -92,9 +92,16 @@ class _ContentManagementPageState extends State ), ), actions: [ + IconButton( + icon: const Icon(Icons.inventory_2_outlined), + tooltip: 'Archived Items', // TODO(you): Will be fixed in l10n phase. + onPressed: () { + context.goNamed(Routes.archivedContentName); + }, + ), IconButton( icon: const Icon(Icons.add), - tooltip: 'Add New Item', // Consider localizing this tooltip + tooltip: 'Add New Item', // TODO(you): Will be fixed in l10n phase. onPressed: () { final currentTab = context .read() From 0c43dc876d4b3fcbe38b281151e2dcb0e27b0ba7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 10:49:24 +0100 Subject: [PATCH 14/48] feat(content_management): add archived content page - Create a new page for viewing and managing archived content - Implement a TabBar for navigating between archived headlines, topics, and sources - Add placeholder text for each tab section - TODO: Implement proper localization for the page title --- .../view/archived_content_page.dart | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 lib/content_management/view/archived_content_page.dart diff --git a/lib/content_management/view/archived_content_page.dart b/lib/content_management/view/archived_content_page.dart new file mode 100644 index 00000000..8bd0ee88 --- /dev/null +++ b/lib/content_management/view/archived_content_page.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/content_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template archived_content_page} +/// A page for viewing and managing archived content. +/// {@endtemplate} +class ArchivedContentPage extends StatefulWidget { + /// {@macro archived_content_page} + const ArchivedContentPage({super.key}); + + @override + State createState() => _ArchivedContentPageState(); +} + +class _ArchivedContentPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Scaffold( + appBar: AppBar( + title: Text('Archived Content'), // TODO(you): Will be fixed in l10n phase. + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab(text: l10n.headlines), + Tab(text: l10n.topics), + Tab(text: l10n.sources), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: const [ + Center(child: Text('Archived Headlines View Placeholder')), + Center(child: Text('Archived Topics View Placeholder')), + Center(child: Text('Archived Sources View Placeholder')), + ], + ), + ); + } +} From 1a164cdb7f057d48e40bb0e5a32cf085731808e4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:02:35 +0100 Subject: [PATCH 15/48] feat(router): add routes for archived headlines, topics, and sources - Replace 'archivedContent' with more specific routes for content management - Add 'archivedHeadlines', 'archivedTopics', and 'archivedSources' routes - Update corresponding route names for each new path --- lib/router/routes.dart | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/router/routes.dart b/lib/router/routes.dart index 66e7c190..60598b8d 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -44,11 +44,23 @@ abstract final class Routes { /// The name for the content management section route. static const String contentManagementName = 'contentManagement'; - /// The path for the archived content page. - static const String archivedContent = 'archived-content'; + /// The path for the archived headlines page. + static const String archivedHeadlines = 'archived-headlines'; - /// The name for the archived content page route. - static const String archivedContentName = 'archivedContent'; + /// The name for the archived headlines page route. + static const String archivedHeadlinesName = 'archivedHeadlines'; + + /// The path for the archived topics page. + static const String archivedTopics = 'archived-topics'; + + /// The name for the archived topics page route. + static const String archivedTopicsName = 'archivedTopics'; + + /// The path for the archived sources page. + static const String archivedSources = 'archived-sources'; + + /// The name for the archived sources page route. + static const String archivedSourcesName = 'archivedSources'; /// The path for creating a new headline. static const String createHeadline = 'create-headline'; From 392cb69cfdf4b6719937bdd298780ffe643427c9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:31:59 +0100 Subject: [PATCH 16/48] feat(content_management): add archived content state - Define ArchivedContentTab enum for available tabs - Define ArchivedContentStatus enum for operation statuses - Create ArchivedContentState class with properties for each tab - Implement copyWith method for state immutability - Override props for Equatable comparison --- .../archived_content_state.dart | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 lib/content_management/bloc/archived_content/archived_content_state.dart diff --git a/lib/content_management/bloc/archived_content/archived_content_state.dart b/lib/content_management/bloc/archived_content/archived_content_state.dart new file mode 100644 index 00000000..f1b8b780 --- /dev/null +++ b/lib/content_management/bloc/archived_content/archived_content_state.dart @@ -0,0 +1,108 @@ +part of 'archived_content_bloc.dart'; + +/// Defines the tabs available in the archived content section. +enum ArchivedContentTab { + /// Represents the Headlines tab. + headlines, + + /// Represents the Topics tab. + topics, + + /// Represents the Sources tab. + sources, +} + +/// Represents the status of archived content operations. +enum ArchivedContentStatus { + initial, + loading, + success, + failure, +} + +/// The state for the archived content feature. +class ArchivedContentState extends Equatable { + const ArchivedContentState({ + this.activeTab = ArchivedContentTab.headlines, + this.headlinesStatus = ArchivedContentStatus.initial, + this.headlines = const [], + this.headlinesCursor, + this.headlinesHasMore = false, + this.topicsStatus = ArchivedContentStatus.initial, + this.topics = const [], + this.topicsCursor, + this.topicsHasMore = false, + this.sourcesStatus = ArchivedContentStatus.initial, + this.sources = const [], + this.sourcesCursor, + this.sourcesHasMore = false, + this.exception, + }); + + final ArchivedContentTab activeTab; + final ArchivedContentStatus headlinesStatus; + final List headlines; + final String? headlinesCursor; + final bool headlinesHasMore; + final ArchivedContentStatus topicsStatus; + final List topics; + final String? topicsCursor; + final bool topicsHasMore; + final ArchivedContentStatus sourcesStatus; + final List sources; + final String? sourcesCursor; + final bool sourcesHasMore; + final HttpException? exception; + + ArchivedContentState copyWith({ + ArchivedContentTab? activeTab, + ArchivedContentStatus? headlinesStatus, + List? headlines, + String? headlinesCursor, + bool? headlinesHasMore, + ArchivedContentStatus? topicsStatus, + List? topics, + String? topicsCursor, + bool? topicsHasMore, + ArchivedContentStatus? sourcesStatus, + List? sources, + String? sourcesCursor, + bool? sourcesHasMore, + HttpException? exception, + }) { + return ArchivedContentState( + activeTab: activeTab ?? this.activeTab, + headlinesStatus: headlinesStatus ?? this.headlinesStatus, + headlines: headlines ?? this.headlines, + headlinesCursor: headlinesCursor ?? this.headlinesCursor, + headlinesHasMore: headlinesHasMore ?? this.headlinesHasMore, + topicsStatus: topicsStatus ?? this.topicsStatus, + topics: topics ?? this.topics, + topicsCursor: topicsCursor ?? this.topicsCursor, + topicsHasMore: topicsHasMore ?? this.topicsHasMore, + sourcesStatus: sourcesStatus ?? this.sourcesStatus, + sources: sources ?? this.sources, + sourcesCursor: sourcesCursor ?? this.sourcesCursor, + sourcesHasMore: sourcesHasMore ?? this.sourcesHasMore, + exception: exception ?? this.exception, + ); + } + + @override + List get props => [ + activeTab, + headlinesStatus, + headlines, + headlinesCursor, + headlinesHasMore, + topicsStatus, + topics, + topicsCursor, + topicsHasMore, + sourcesStatus, + sources, + sourcesCursor, + sourcesHasMore, + exception, + ]; +} From 6c723b610813a4db59a501826851964e64e1e281 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:32:11 +0100 Subject: [PATCH 17/48] feat(content_management): add archived content events - Create ArchivedContentEvent base class - Add events for tab changes and loading archived content - Implement restore and delete events for headlines, topics, and sources --- .../archived_content_event.dart | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 lib/content_management/bloc/archived_content/archived_content_event.dart diff --git a/lib/content_management/bloc/archived_content/archived_content_event.dart b/lib/content_management/bloc/archived_content/archived_content_event.dart new file mode 100644 index 00000000..88aba8d4 --- /dev/null +++ b/lib/content_management/bloc/archived_content/archived_content_event.dart @@ -0,0 +1,91 @@ +part of 'archived_content_bloc.dart'; + +sealed class ArchivedContentEvent extends Equatable { + const ArchivedContentEvent(); + + @override + List get props => []; +} + +/// Event to change the active archived content tab. +final class ArchivedContentTabChanged extends ArchivedContentEvent { + const ArchivedContentTabChanged(this.tab); + + final ArchivedContentTab tab; + + @override + List get props => [tab]; +} + +/// Event to request loading of archived headlines. +final class LoadArchivedHeadlinesRequested extends ArchivedContentEvent { + const LoadArchivedHeadlinesRequested({this.startAfterId, this.limit}); + + final String? startAfterId; + final int? limit; + + @override + List get props => [startAfterId, limit]; +} + +/// Event to request loading of archived topics. +final class LoadArchivedTopicsRequested extends ArchivedContentEvent { + const LoadArchivedTopicsRequested({this.startAfterId, this.limit}); + + final String? startAfterId; + final int? limit; + + @override + List get props => [startAfterId, limit]; +} + +/// Event to request loading of archived sources. +final class LoadArchivedSourcesRequested extends ArchivedContentEvent { + const LoadArchivedSourcesRequested({this.startAfterId, this.limit}); + + final String? startAfterId; + final int? limit; + + @override + List get props => [startAfterId, limit]; +} + +/// Event to restore an archived headline. +final class RestoreHeadlineRequested extends ArchivedContentEvent { + const RestoreHeadlineRequested(this.id); + + final String id; + + @override + List get props => [id]; +} + +/// Event to restore an archived topic. +final class RestoreTopicRequested extends ArchivedContentEvent { + const RestoreTopicRequested(this.id); + + final String id; + + @override + List get props => [id]; +} + +/// Event to restore an archived source. +final class RestoreSourceRequested extends ArchivedContentEvent { + const RestoreSourceRequested(this.id); + + final String id; + + @override + List get props => [id]; +} + +/// Event to permanently delete an archived headline. +final class DeleteHeadlineForeverRequested extends ArchivedContentEvent { + const DeleteHeadlineForeverRequested(this.id); + + final String id; + + @override + List get props => [id]; +} From b00cf36f9903e78a45b0d37e3190b56f66fc45e3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:33:12 +0100 Subject: [PATCH 18/48] feat(archived_content): implement ArchivedContentBloc - Add ArchivedContentBloc for managing archived content - Implement logic for handling headlines, topics, and sources - Include functionality for loading, restoring, and deleting archived items - Handle pagination and state management for archived content --- .../archived_content_bloc.dart | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 lib/content_management/bloc/archived_content/archived_content_bloc.dart diff --git a/lib/content_management/bloc/archived_content/archived_content_bloc.dart b/lib/content_management/bloc/archived_content/archived_content_bloc.dart new file mode 100644 index 00000000..0ae0f946 --- /dev/null +++ b/lib/content_management/bloc/archived_content/archived_content_bloc.dart @@ -0,0 +1,280 @@ +import 'package:bloc/bloc.dart'; +import 'package:core/core.dart'; +import 'package:data_repository/data_repository.dart'; +import 'package:equatable/equatable.dart'; + +part 'archived_content_event.dart'; +part 'archived_content_state.dart'; + +class ArchivedContentBloc + extends Bloc { + ArchivedContentBloc({ + required DataRepository headlinesRepository, + required DataRepository topicsRepository, + required DataRepository sourcesRepository, + }) : _headlinesRepository = headlinesRepository, + _topicsRepository = topicsRepository, + _sourcesRepository = sourcesRepository, + super(const ArchivedContentState()) { + on(_onArchivedContentTabChanged); + on(_onLoadArchivedHeadlinesRequested); + on(_onRestoreHeadlineRequested); + on(_onDeleteHeadlineForeverRequested); + on(_onLoadArchivedTopicsRequested); + on(_onRestoreTopicRequested); + on(_onLoadArchivedSourcesRequested); + on(_onRestoreSourceRequested); + } + + final DataRepository _headlinesRepository; + final DataRepository _topicsRepository; + final DataRepository _sourcesRepository; + + void _onArchivedContentTabChanged( + ArchivedContentTabChanged event, + Emitter emit, + ) { + emit(state.copyWith(activeTab: event.tab)); + } + + Future _onLoadArchivedHeadlinesRequested( + LoadArchivedHeadlinesRequested event, + Emitter emit, + ) async { + emit(state.copyWith(headlinesStatus: ArchivedContentStatus.loading)); + try { + final isPaginating = event.startAfterId != null; + final previousHeadlines = isPaginating ? state.headlines : []; + + final paginatedHeadlines = await _headlinesRepository.readAll( + filter: {'status': ContentStatus.archived.name}, + sort: [const SortOption('updatedAt', SortOrder.desc)], + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), + ); + emit( + state.copyWith( + headlinesStatus: ArchivedContentStatus.success, + headlines: [...previousHeadlines, ...paginatedHeadlines.items], + headlinesCursor: paginatedHeadlines.cursor, + headlinesHasMore: paginatedHeadlines.hasMore, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + headlinesStatus: ArchivedContentStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + headlinesStatus: ArchivedContentStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + Future _onRestoreHeadlineRequested( + RestoreHeadlineRequested event, + Emitter emit, + ) async { + final originalHeadlines = List.from(state.headlines); + final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id); + if (headlineIndex == -1) return; + + final headlineToRestore = originalHeadlines[headlineIndex]; + final updatedHeadlines = originalHeadlines..removeAt(headlineIndex); + + emit(state.copyWith(headlines: updatedHeadlines)); + + try { + await _headlinesRepository.update( + id: event.id, + item: headlineToRestore.copyWith(status: ContentStatus.active), + ); + } on HttpException catch (e) { + emit(state.copyWith(headlines: originalHeadlines, exception: e)); + } catch (e) { + emit( + state.copyWith( + headlines: originalHeadlines, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + Future _onDeleteHeadlineForeverRequested( + DeleteHeadlineForeverRequested event, + Emitter emit, + ) async { + final originalHeadlines = List.from(state.headlines); + final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id); + if (headlineIndex == -1) return; + + final updatedHeadlines = originalHeadlines..removeAt(headlineIndex); + emit(state.copyWith(headlines: updatedHeadlines)); + + try { + await _headlinesRepository.delete(id: event.id); + } on HttpException catch (e) { + emit(state.copyWith(headlines: originalHeadlines, exception: e)); + } catch (e) { + emit( + state.copyWith( + headlines: originalHeadlines, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + Future _onLoadArchivedTopicsRequested( + LoadArchivedTopicsRequested event, + Emitter emit, + ) async { + emit(state.copyWith(topicsStatus: ArchivedContentStatus.loading)); + try { + final isPaginating = event.startAfterId != null; + final previousTopics = isPaginating ? state.topics : []; + + final paginatedTopics = await _topicsRepository.readAll( + filter: {'status': ContentStatus.archived.name}, + sort: [const SortOption('updatedAt', SortOrder.desc)], + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), + ); + emit( + state.copyWith( + topicsStatus: ArchivedContentStatus.success, + topics: [...previousTopics, ...paginatedTopics.items], + topicsCursor: paginatedTopics.cursor, + topicsHasMore: paginatedTopics.hasMore, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + topicsStatus: ArchivedContentStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + topicsStatus: ArchivedContentStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + Future _onRestoreTopicRequested( + RestoreTopicRequested event, + Emitter emit, + ) async { + final originalTopics = List.from(state.topics); + final topicIndex = originalTopics.indexWhere((t) => t.id == event.id); + if (topicIndex == -1) return; + + final topicToRestore = originalTopics[topicIndex]; + final updatedTopics = originalTopics..removeAt(topicIndex); + + emit(state.copyWith(topics: updatedTopics)); + + try { + await _topicsRepository.update( + id: event.id, + item: topicToRestore.copyWith(status: ContentStatus.active), + ); + } on HttpException catch (e) { + emit(state.copyWith(topics: originalTopics, exception: e)); + } catch (e) { + emit( + state.copyWith( + topics: originalTopics, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + Future _onLoadArchivedSourcesRequested( + LoadArchivedSourcesRequested event, + Emitter emit, + ) async { + emit(state.copyWith(sourcesStatus: ArchivedContentStatus.loading)); + try { + final isPaginating = event.startAfterId != null; + final previousSources = isPaginating ? state.sources : []; + + final paginatedSources = await _sourcesRepository.readAll( + filter: {'status': ContentStatus.archived.name}, + sort: [const SortOption('updatedAt', SortOrder.desc)], + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), + ); + emit( + state.copyWith( + sourcesStatus: ArchivedContentStatus.success, + sources: [...previousSources, ...paginatedSources.items], + sourcesCursor: paginatedSources.cursor, + sourcesHasMore: paginatedSources.hasMore, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + sourcesStatus: ArchivedContentStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + sourcesStatus: ArchivedContentStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + Future _onRestoreSourceRequested( + RestoreSourceRequested event, + Emitter emit, + ) async { + final originalSources = List.from(state.sources); + final sourceIndex = originalSources.indexWhere((s) => s.id == event.id); + if (sourceIndex == -1) return; + + final sourceToRestore = originalSources[sourceIndex]; + final updatedSources = originalSources..removeAt(sourceIndex); + + emit(state.copyWith(sources: updatedSources)); + + try { + await _sourcesRepository.update( + id: event.id, + item: sourceToRestore.copyWith(status: ContentStatus.active), + ); + } on HttpException catch (e) { + emit(state.copyWith(sources: originalSources, exception: e)); + } catch (e) { + emit( + state.copyWith( + sources: originalSources, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } +} From 7b8fae048860f4e6ed4df0f7eb00a0efe6083d9f Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:35:46 +0100 Subject: [PATCH 19/48] refactor(content_management): remove unused archived content bloc - Remove ArchivedContentBloc and related files - Delete archived_content_bloc.dart, archived_content_event.dart, and archived_content_state.dart - This refactoring removes the entire bloc responsible for managing archived content, which is no longer in use --- .../archived_content_bloc.dart | 280 ------------------ .../archived_content_event.dart | 91 ------ .../archived_content_state.dart | 108 ------- 3 files changed, 479 deletions(-) delete mode 100644 lib/content_management/bloc/archived_content/archived_content_bloc.dart delete mode 100644 lib/content_management/bloc/archived_content/archived_content_event.dart delete mode 100644 lib/content_management/bloc/archived_content/archived_content_state.dart diff --git a/lib/content_management/bloc/archived_content/archived_content_bloc.dart b/lib/content_management/bloc/archived_content/archived_content_bloc.dart deleted file mode 100644 index 0ae0f946..00000000 --- a/lib/content_management/bloc/archived_content/archived_content_bloc.dart +++ /dev/null @@ -1,280 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:core/core.dart'; -import 'package:data_repository/data_repository.dart'; -import 'package:equatable/equatable.dart'; - -part 'archived_content_event.dart'; -part 'archived_content_state.dart'; - -class ArchivedContentBloc - extends Bloc { - ArchivedContentBloc({ - required DataRepository headlinesRepository, - required DataRepository topicsRepository, - required DataRepository sourcesRepository, - }) : _headlinesRepository = headlinesRepository, - _topicsRepository = topicsRepository, - _sourcesRepository = sourcesRepository, - super(const ArchivedContentState()) { - on(_onArchivedContentTabChanged); - on(_onLoadArchivedHeadlinesRequested); - on(_onRestoreHeadlineRequested); - on(_onDeleteHeadlineForeverRequested); - on(_onLoadArchivedTopicsRequested); - on(_onRestoreTopicRequested); - on(_onLoadArchivedSourcesRequested); - on(_onRestoreSourceRequested); - } - - final DataRepository _headlinesRepository; - final DataRepository _topicsRepository; - final DataRepository _sourcesRepository; - - void _onArchivedContentTabChanged( - ArchivedContentTabChanged event, - Emitter emit, - ) { - emit(state.copyWith(activeTab: event.tab)); - } - - Future _onLoadArchivedHeadlinesRequested( - LoadArchivedHeadlinesRequested event, - Emitter emit, - ) async { - emit(state.copyWith(headlinesStatus: ArchivedContentStatus.loading)); - try { - final isPaginating = event.startAfterId != null; - final previousHeadlines = isPaginating ? state.headlines : []; - - final paginatedHeadlines = await _headlinesRepository.readAll( - filter: {'status': ContentStatus.archived.name}, - sort: [const SortOption('updatedAt', SortOrder.desc)], - pagination: PaginationOptions( - cursor: event.startAfterId, - limit: event.limit, - ), - ); - emit( - state.copyWith( - headlinesStatus: ArchivedContentStatus.success, - headlines: [...previousHeadlines, ...paginatedHeadlines.items], - headlinesCursor: paginatedHeadlines.cursor, - headlinesHasMore: paginatedHeadlines.hasMore, - ), - ); - } on HttpException catch (e) { - emit( - state.copyWith( - headlinesStatus: ArchivedContentStatus.failure, - exception: e, - ), - ); - } catch (e) { - emit( - state.copyWith( - headlinesStatus: ArchivedContentStatus.failure, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - } - } - - Future _onRestoreHeadlineRequested( - RestoreHeadlineRequested event, - Emitter emit, - ) async { - final originalHeadlines = List.from(state.headlines); - final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id); - if (headlineIndex == -1) return; - - final headlineToRestore = originalHeadlines[headlineIndex]; - final updatedHeadlines = originalHeadlines..removeAt(headlineIndex); - - emit(state.copyWith(headlines: updatedHeadlines)); - - try { - await _headlinesRepository.update( - id: event.id, - item: headlineToRestore.copyWith(status: ContentStatus.active), - ); - } on HttpException catch (e) { - emit(state.copyWith(headlines: originalHeadlines, exception: e)); - } catch (e) { - emit( - state.copyWith( - headlines: originalHeadlines, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - } - } - - Future _onDeleteHeadlineForeverRequested( - DeleteHeadlineForeverRequested event, - Emitter emit, - ) async { - final originalHeadlines = List.from(state.headlines); - final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id); - if (headlineIndex == -1) return; - - final updatedHeadlines = originalHeadlines..removeAt(headlineIndex); - emit(state.copyWith(headlines: updatedHeadlines)); - - try { - await _headlinesRepository.delete(id: event.id); - } on HttpException catch (e) { - emit(state.copyWith(headlines: originalHeadlines, exception: e)); - } catch (e) { - emit( - state.copyWith( - headlines: originalHeadlines, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - } - } - - Future _onLoadArchivedTopicsRequested( - LoadArchivedTopicsRequested event, - Emitter emit, - ) async { - emit(state.copyWith(topicsStatus: ArchivedContentStatus.loading)); - try { - final isPaginating = event.startAfterId != null; - final previousTopics = isPaginating ? state.topics : []; - - final paginatedTopics = await _topicsRepository.readAll( - filter: {'status': ContentStatus.archived.name}, - sort: [const SortOption('updatedAt', SortOrder.desc)], - pagination: PaginationOptions( - cursor: event.startAfterId, - limit: event.limit, - ), - ); - emit( - state.copyWith( - topicsStatus: ArchivedContentStatus.success, - topics: [...previousTopics, ...paginatedTopics.items], - topicsCursor: paginatedTopics.cursor, - topicsHasMore: paginatedTopics.hasMore, - ), - ); - } on HttpException catch (e) { - emit( - state.copyWith( - topicsStatus: ArchivedContentStatus.failure, - exception: e, - ), - ); - } catch (e) { - emit( - state.copyWith( - topicsStatus: ArchivedContentStatus.failure, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - } - } - - Future _onRestoreTopicRequested( - RestoreTopicRequested event, - Emitter emit, - ) async { - final originalTopics = List.from(state.topics); - final topicIndex = originalTopics.indexWhere((t) => t.id == event.id); - if (topicIndex == -1) return; - - final topicToRestore = originalTopics[topicIndex]; - final updatedTopics = originalTopics..removeAt(topicIndex); - - emit(state.copyWith(topics: updatedTopics)); - - try { - await _topicsRepository.update( - id: event.id, - item: topicToRestore.copyWith(status: ContentStatus.active), - ); - } on HttpException catch (e) { - emit(state.copyWith(topics: originalTopics, exception: e)); - } catch (e) { - emit( - state.copyWith( - topics: originalTopics, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - } - } - - Future _onLoadArchivedSourcesRequested( - LoadArchivedSourcesRequested event, - Emitter emit, - ) async { - emit(state.copyWith(sourcesStatus: ArchivedContentStatus.loading)); - try { - final isPaginating = event.startAfterId != null; - final previousSources = isPaginating ? state.sources : []; - - final paginatedSources = await _sourcesRepository.readAll( - filter: {'status': ContentStatus.archived.name}, - sort: [const SortOption('updatedAt', SortOrder.desc)], - pagination: PaginationOptions( - cursor: event.startAfterId, - limit: event.limit, - ), - ); - emit( - state.copyWith( - sourcesStatus: ArchivedContentStatus.success, - sources: [...previousSources, ...paginatedSources.items], - sourcesCursor: paginatedSources.cursor, - sourcesHasMore: paginatedSources.hasMore, - ), - ); - } on HttpException catch (e) { - emit( - state.copyWith( - sourcesStatus: ArchivedContentStatus.failure, - exception: e, - ), - ); - } catch (e) { - emit( - state.copyWith( - sourcesStatus: ArchivedContentStatus.failure, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - } - } - - Future _onRestoreSourceRequested( - RestoreSourceRequested event, - Emitter emit, - ) async { - final originalSources = List.from(state.sources); - final sourceIndex = originalSources.indexWhere((s) => s.id == event.id); - if (sourceIndex == -1) return; - - final sourceToRestore = originalSources[sourceIndex]; - final updatedSources = originalSources..removeAt(sourceIndex); - - emit(state.copyWith(sources: updatedSources)); - - try { - await _sourcesRepository.update( - id: event.id, - item: sourceToRestore.copyWith(status: ContentStatus.active), - ); - } on HttpException catch (e) { - emit(state.copyWith(sources: originalSources, exception: e)); - } catch (e) { - emit( - state.copyWith( - sources: originalSources, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - } - } -} diff --git a/lib/content_management/bloc/archived_content/archived_content_event.dart b/lib/content_management/bloc/archived_content/archived_content_event.dart deleted file mode 100644 index 88aba8d4..00000000 --- a/lib/content_management/bloc/archived_content/archived_content_event.dart +++ /dev/null @@ -1,91 +0,0 @@ -part of 'archived_content_bloc.dart'; - -sealed class ArchivedContentEvent extends Equatable { - const ArchivedContentEvent(); - - @override - List get props => []; -} - -/// Event to change the active archived content tab. -final class ArchivedContentTabChanged extends ArchivedContentEvent { - const ArchivedContentTabChanged(this.tab); - - final ArchivedContentTab tab; - - @override - List get props => [tab]; -} - -/// Event to request loading of archived headlines. -final class LoadArchivedHeadlinesRequested extends ArchivedContentEvent { - const LoadArchivedHeadlinesRequested({this.startAfterId, this.limit}); - - final String? startAfterId; - final int? limit; - - @override - List get props => [startAfterId, limit]; -} - -/// Event to request loading of archived topics. -final class LoadArchivedTopicsRequested extends ArchivedContentEvent { - const LoadArchivedTopicsRequested({this.startAfterId, this.limit}); - - final String? startAfterId; - final int? limit; - - @override - List get props => [startAfterId, limit]; -} - -/// Event to request loading of archived sources. -final class LoadArchivedSourcesRequested extends ArchivedContentEvent { - const LoadArchivedSourcesRequested({this.startAfterId, this.limit}); - - final String? startAfterId; - final int? limit; - - @override - List get props => [startAfterId, limit]; -} - -/// Event to restore an archived headline. -final class RestoreHeadlineRequested extends ArchivedContentEvent { - const RestoreHeadlineRequested(this.id); - - final String id; - - @override - List get props => [id]; -} - -/// Event to restore an archived topic. -final class RestoreTopicRequested extends ArchivedContentEvent { - const RestoreTopicRequested(this.id); - - final String id; - - @override - List get props => [id]; -} - -/// Event to restore an archived source. -final class RestoreSourceRequested extends ArchivedContentEvent { - const RestoreSourceRequested(this.id); - - final String id; - - @override - List get props => [id]; -} - -/// Event to permanently delete an archived headline. -final class DeleteHeadlineForeverRequested extends ArchivedContentEvent { - const DeleteHeadlineForeverRequested(this.id); - - final String id; - - @override - List get props => [id]; -} diff --git a/lib/content_management/bloc/archived_content/archived_content_state.dart b/lib/content_management/bloc/archived_content/archived_content_state.dart deleted file mode 100644 index f1b8b780..00000000 --- a/lib/content_management/bloc/archived_content/archived_content_state.dart +++ /dev/null @@ -1,108 +0,0 @@ -part of 'archived_content_bloc.dart'; - -/// Defines the tabs available in the archived content section. -enum ArchivedContentTab { - /// Represents the Headlines tab. - headlines, - - /// Represents the Topics tab. - topics, - - /// Represents the Sources tab. - sources, -} - -/// Represents the status of archived content operations. -enum ArchivedContentStatus { - initial, - loading, - success, - failure, -} - -/// The state for the archived content feature. -class ArchivedContentState extends Equatable { - const ArchivedContentState({ - this.activeTab = ArchivedContentTab.headlines, - this.headlinesStatus = ArchivedContentStatus.initial, - this.headlines = const [], - this.headlinesCursor, - this.headlinesHasMore = false, - this.topicsStatus = ArchivedContentStatus.initial, - this.topics = const [], - this.topicsCursor, - this.topicsHasMore = false, - this.sourcesStatus = ArchivedContentStatus.initial, - this.sources = const [], - this.sourcesCursor, - this.sourcesHasMore = false, - this.exception, - }); - - final ArchivedContentTab activeTab; - final ArchivedContentStatus headlinesStatus; - final List headlines; - final String? headlinesCursor; - final bool headlinesHasMore; - final ArchivedContentStatus topicsStatus; - final List topics; - final String? topicsCursor; - final bool topicsHasMore; - final ArchivedContentStatus sourcesStatus; - final List sources; - final String? sourcesCursor; - final bool sourcesHasMore; - final HttpException? exception; - - ArchivedContentState copyWith({ - ArchivedContentTab? activeTab, - ArchivedContentStatus? headlinesStatus, - List? headlines, - String? headlinesCursor, - bool? headlinesHasMore, - ArchivedContentStatus? topicsStatus, - List? topics, - String? topicsCursor, - bool? topicsHasMore, - ArchivedContentStatus? sourcesStatus, - List? sources, - String? sourcesCursor, - bool? sourcesHasMore, - HttpException? exception, - }) { - return ArchivedContentState( - activeTab: activeTab ?? this.activeTab, - headlinesStatus: headlinesStatus ?? this.headlinesStatus, - headlines: headlines ?? this.headlines, - headlinesCursor: headlinesCursor ?? this.headlinesCursor, - headlinesHasMore: headlinesHasMore ?? this.headlinesHasMore, - topicsStatus: topicsStatus ?? this.topicsStatus, - topics: topics ?? this.topics, - topicsCursor: topicsCursor ?? this.topicsCursor, - topicsHasMore: topicsHasMore ?? this.topicsHasMore, - sourcesStatus: sourcesStatus ?? this.sourcesStatus, - sources: sources ?? this.sources, - sourcesCursor: sourcesCursor ?? this.sourcesCursor, - sourcesHasMore: sourcesHasMore ?? this.sourcesHasMore, - exception: exception ?? this.exception, - ); - } - - @override - List get props => [ - activeTab, - headlinesStatus, - headlines, - headlinesCursor, - headlinesHasMore, - topicsStatus, - topics, - topicsCursor, - topicsHasMore, - sourcesStatus, - sources, - sourcesCursor, - sourcesHasMore, - exception, - ]; -} From 225c29f78d8eba3bba218b0e0dac801e7431173f Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:36:56 +0100 Subject: [PATCH 20/48] feat(content_management): add archived headlines bloc - Create ArchivedHeadlinesBloc class with initial state - Define ArchivedHeadlinesEvent and ArchivedHeadlinesState classes - Set up basic structure for handling archived headlines events --- .../archived_headlines/archived_headlines_bloc.dart | 13 +++++++++++++ .../archived_headlines_event.dart | 8 ++++++++ .../archived_headlines_state.dart | 10 ++++++++++ 3 files changed, 31 insertions(+) create mode 100644 lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart create mode 100644 lib/content_management/bloc/archived_headlines/archived_headlines_event.dart create mode 100644 lib/content_management/bloc/archived_headlines/archived_headlines_state.dart diff --git a/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart b/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart new file mode 100644 index 00000000..ccbe3445 --- /dev/null +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart @@ -0,0 +1,13 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'archived_headlines_event.dart'; +part 'archived_headlines_state.dart'; + +class ArchivedHeadlinesBloc extends Bloc { + ArchivedHeadlinesBloc() : super(ArchivedHeadlinesInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} diff --git a/lib/content_management/bloc/archived_headlines/archived_headlines_event.dart b/lib/content_management/bloc/archived_headlines/archived_headlines_event.dart new file mode 100644 index 00000000..820be1ca --- /dev/null +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_event.dart @@ -0,0 +1,8 @@ +part of 'archived_headlines_bloc.dart'; + +sealed class ArchivedHeadlinesEvent extends Equatable { + const ArchivedHeadlinesEvent(); + + @override + List get props => []; +} diff --git a/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart b/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart new file mode 100644 index 00000000..4bf79aee --- /dev/null +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart @@ -0,0 +1,10 @@ +part of 'archived_headlines_bloc.dart'; + +sealed class ArchivedHeadlinesState extends Equatable { + const ArchivedHeadlinesState(); + + @override + List get props => []; +} + +final class ArchivedHeadlinesInitial extends ArchivedHeadlinesState {} From 439d5e06e5ede87047041c9920b5d3105c37b9d7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:37:12 +0100 Subject: [PATCH 21/48] feat(content_management): add archived topics bloc - Create ArchivedTopicsBloc, ArchivedTopicsEvent, and ArchivedTopicsState classes - Implement basic structure for handling archived topics using bloc pattern --- .../bloc/archived_topics/archived_topics_bloc.dart | 13 +++++++++++++ .../bloc/archived_topics/archived_topics_event.dart | 8 ++++++++ .../bloc/archived_topics/archived_topics_state.dart | 10 ++++++++++ 3 files changed, 31 insertions(+) create mode 100644 lib/content_management/bloc/archived_topics/archived_topics_bloc.dart create mode 100644 lib/content_management/bloc/archived_topics/archived_topics_event.dart create mode 100644 lib/content_management/bloc/archived_topics/archived_topics_state.dart diff --git a/lib/content_management/bloc/archived_topics/archived_topics_bloc.dart b/lib/content_management/bloc/archived_topics/archived_topics_bloc.dart new file mode 100644 index 00000000..ad708cc7 --- /dev/null +++ b/lib/content_management/bloc/archived_topics/archived_topics_bloc.dart @@ -0,0 +1,13 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'archived_topics_event.dart'; +part 'archived_topics_state.dart'; + +class ArchivedTopicsBloc extends Bloc { + ArchivedTopicsBloc() : super(ArchivedTopicsInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} diff --git a/lib/content_management/bloc/archived_topics/archived_topics_event.dart b/lib/content_management/bloc/archived_topics/archived_topics_event.dart new file mode 100644 index 00000000..ab77de31 --- /dev/null +++ b/lib/content_management/bloc/archived_topics/archived_topics_event.dart @@ -0,0 +1,8 @@ +part of 'archived_topics_bloc.dart'; + +sealed class ArchivedTopicsEvent extends Equatable { + const ArchivedTopicsEvent(); + + @override + List get props => []; +} diff --git a/lib/content_management/bloc/archived_topics/archived_topics_state.dart b/lib/content_management/bloc/archived_topics/archived_topics_state.dart new file mode 100644 index 00000000..3e7ee029 --- /dev/null +++ b/lib/content_management/bloc/archived_topics/archived_topics_state.dart @@ -0,0 +1,10 @@ +part of 'archived_topics_bloc.dart'; + +sealed class ArchivedTopicsState extends Equatable { + const ArchivedTopicsState(); + + @override + List get props => []; +} + +final class ArchivedTopicsInitial extends ArchivedTopicsState {} From 5a06895efe834d6791c1854cf8c3507bdd6d3aba Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:38:58 +0100 Subject: [PATCH 22/48] feat(content_management): add archived sources bloc - Create ArchivedSourcesBloc class - Define ArchivedSourcesEvent sealed class - Define ArchivedSourcesState sealed class - Add ArchivedSourcesInitial state --- .../archived_sources/archived_sources_bloc.dart | 13 +++++++++++++ .../archived_sources/archived_sources_event.dart | 8 ++++++++ .../archived_sources/archived_sources_state.dart | 10 ++++++++++ 3 files changed, 31 insertions(+) create mode 100644 lib/content_management/bloc/archived_sources/archived_sources_bloc.dart create mode 100644 lib/content_management/bloc/archived_sources/archived_sources_event.dart create mode 100644 lib/content_management/bloc/archived_sources/archived_sources_state.dart diff --git a/lib/content_management/bloc/archived_sources/archived_sources_bloc.dart b/lib/content_management/bloc/archived_sources/archived_sources_bloc.dart new file mode 100644 index 00000000..2d8fb0eb --- /dev/null +++ b/lib/content_management/bloc/archived_sources/archived_sources_bloc.dart @@ -0,0 +1,13 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'archived_sources_event.dart'; +part 'archived_sources_state.dart'; + +class ArchivedSourcesBloc extends Bloc { + ArchivedSourcesBloc() : super(ArchivedSourcesInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} diff --git a/lib/content_management/bloc/archived_sources/archived_sources_event.dart b/lib/content_management/bloc/archived_sources/archived_sources_event.dart new file mode 100644 index 00000000..ca3ccff3 --- /dev/null +++ b/lib/content_management/bloc/archived_sources/archived_sources_event.dart @@ -0,0 +1,8 @@ +part of 'archived_sources_bloc.dart'; + +sealed class ArchivedSourcesEvent extends Equatable { + const ArchivedSourcesEvent(); + + @override + List get props => []; +} diff --git a/lib/content_management/bloc/archived_sources/archived_sources_state.dart b/lib/content_management/bloc/archived_sources/archived_sources_state.dart new file mode 100644 index 00000000..d542f5de --- /dev/null +++ b/lib/content_management/bloc/archived_sources/archived_sources_state.dart @@ -0,0 +1,10 @@ +part of 'archived_sources_bloc.dart'; + +sealed class ArchivedSourcesState extends Equatable { + const ArchivedSourcesState(); + + @override + List get props => []; +} + +final class ArchivedSourcesInitial extends ArchivedSourcesState {} From ac2bd91ef9d713a59858888283a59f1d4686c2ab Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:39:08 +0100 Subject: [PATCH 23/48] refactor(content_management): remove archived content page - Remove ArchivedContentPage widget and its related code - This page was likely unused and incomplete, as indicated by the TODO comment and placeholder text --- .../view/archived_content_page.dart | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 lib/content_management/view/archived_content_page.dart diff --git a/lib/content_management/view/archived_content_page.dart b/lib/content_management/view/archived_content_page.dart deleted file mode 100644 index 8bd0ee88..00000000 --- a/lib/content_management/view/archived_content_page.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/content_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:ui_kit/ui_kit.dart'; - -/// {@template archived_content_page} -/// A page for viewing and managing archived content. -/// {@endtemplate} -class ArchivedContentPage extends StatefulWidget { - /// {@macro archived_content_page} - const ArchivedContentPage({super.key}); - - @override - State createState() => _ArchivedContentPageState(); -} - -class _ArchivedContentPageState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 3, vsync: this); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - return Scaffold( - appBar: AppBar( - title: Text('Archived Content'), // TODO(you): Will be fixed in l10n phase. - bottom: TabBar( - controller: _tabController, - tabs: [ - Tab(text: l10n.headlines), - Tab(text: l10n.topics), - Tab(text: l10n.sources), - ], - ), - ), - body: TabBarView( - controller: _tabController, - children: const [ - Center(child: Text('Archived Headlines View Placeholder')), - Center(child: Text('Archived Topics View Placeholder')), - Center(child: Text('Archived Sources View Placeholder')), - ], - ), - ); - } -} From 1aee2f422f3b96cfa7378217b7c2107033cd6a98 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:42:33 +0100 Subject: [PATCH 24/48] feat(content_management): navigate to respective archived pages - Update archived items button to navigate to different archived pages based on the active tab in ContentManagementBloc - Replace single navigation route with conditional navigation to archivedHeadlines, archivedTopics, or archivedSources based on the active tab --- .../view/content_management_page.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/content_management_page.dart b/lib/content_management/view/content_management_page.dart index ab8a1eb7..7a065052 100644 --- a/lib/content_management/view/content_management_page.dart +++ b/lib/content_management/view/content_management_page.dart @@ -96,7 +96,16 @@ class _ContentManagementPageState extends State icon: const Icon(Icons.inventory_2_outlined), tooltip: 'Archived Items', // TODO(you): Will be fixed in l10n phase. onPressed: () { - context.goNamed(Routes.archivedContentName); + final currentTab = + context.read().state.activeTab; + switch (currentTab) { + case ContentManagementTab.headlines: + context.goNamed(Routes.archivedHeadlinesName); + case ContentManagementTab.topics: + context.goNamed(Routes.archivedTopicsName); + case ContentManagementTab.sources: + context.goNamed(Routes.archivedSourcesName); + } }, ), IconButton( From 69fddcd67cbc18da2fee69015b9e13953b271172 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:45:10 +0100 Subject: [PATCH 25/48] feat(router): add archived content routes and placeholders - Remove ArchivedContentPage import and route - Add separate routes for archived headlines, topics, and sources - Implement placeholder pages for new archived content routes --- lib/router/router.dart | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/router/router.dart b/lib/router/router.dart index ca684f33..151a0754 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -17,7 +17,6 @@ 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_headline_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/view/edit_source_page.dart'; 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/view/archived_content_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/view/dashboard_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'; @@ -198,9 +197,22 @@ GoRouter createRouter({ }, ), GoRoute( - path: Routes.archivedContent, - name: Routes.archivedContentName, - builder: (context, state) => const ArchivedContentPage(), + path: Routes.archivedHeadlines, + name: Routes.archivedHeadlinesName, + builder: (context, state) => + const Placeholder(), + ), + GoRoute( + path: Routes.archivedTopics, + name: Routes.archivedTopicsName, + builder: (context, state) => + const Placeholder(), + ), + GoRoute( + path: Routes.archivedSources, + name: Routes.archivedSourcesName, + builder: (context, state) => + const Placeholder(), ), ], ), From deb091e0fa8bbc8b8cfa8816e3d156a7917c3301 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:49:56 +0100 Subject: [PATCH 26/48] feat(content_management): add archived headlines events - Add LoadArchivedHeadlinesRequested event to request loading of archived headlines - Add RestoreHeadlineRequested event to restore an archived headline - Add DeleteHeadlineForeverRequested event to permanently delete an archived headline - Update ArchivedHeadlinesEvent props to allow nullable objects --- .../archived_headlines_event.dart | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/content_management/bloc/archived_headlines/archived_headlines_event.dart b/lib/content_management/bloc/archived_headlines/archived_headlines_event.dart index 820be1ca..e191954e 100644 --- a/lib/content_management/bloc/archived_headlines/archived_headlines_event.dart +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_event.dart @@ -4,5 +4,36 @@ sealed class ArchivedHeadlinesEvent extends Equatable { const ArchivedHeadlinesEvent(); @override - List get props => []; + List get props => []; +} + +/// Event to request loading of archived headlines. +final class LoadArchivedHeadlinesRequested extends ArchivedHeadlinesEvent { + const LoadArchivedHeadlinesRequested({this.startAfterId, this.limit}); + + final String? startAfterId; + final int? limit; + + @override + List get props => [startAfterId, limit]; +} + +/// Event to restore an archived headline. +final class RestoreHeadlineRequested extends ArchivedHeadlinesEvent { + const RestoreHeadlineRequested(this.id); + + final String id; + + @override + List get props => [id]; +} + +/// Event to permanently delete an archived headline. +final class DeleteHeadlineForeverRequested extends ArchivedHeadlinesEvent { + const DeleteHeadlineForeverRequested(this.id); + + final String id; + + @override + List get props => [id]; } From c8523f2547d138ab51d5d75211d8365ad16f26f3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:50:07 +0100 Subject: [PATCH 27/48] refactor(content_management): enhance archived headlines state management - Replace sealed class with enum for status representation - Expand ArchivedHeadlinesState with additional properties - Implement copyWith method for state immutability - Update props list for Equatable --- .../archived_headlines_state.dart | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart b/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart index 4bf79aee..1f900012 100644 --- a/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart @@ -1,10 +1,51 @@ part of 'archived_headlines_bloc.dart'; -sealed class ArchivedHeadlinesState extends Equatable { - const ArchivedHeadlinesState(); - - @override - List get props => []; +/// Represents the status of archived content operations. +enum ArchivedHeadlinesStatus { + initial, + loading, + success, + failure, } -final class ArchivedHeadlinesInitial extends ArchivedHeadlinesState {} +/// The state for the archived content feature. +class ArchivedHeadlinesState extends Equatable { + const ArchivedHeadlinesState({ + this.status = ArchivedHeadlinesStatus.initial, + this.headlines = const [], + this.cursor, + this.hasMore = false, + this.exception, + }); + + final ArchivedHeadlinesStatus status; + final List headlines; + final String? cursor; + final bool hasMore; + final HttpException? exception; + + ArchivedHeadlinesState copyWith({ + ArchivedHeadlinesStatus? status, + List? headlines, + String? cursor, + bool? hasMore, + HttpException? exception, + }) { + return ArchivedHeadlinesState( + status: status ?? this.status, + headlines: headlines ?? this.headlines, + cursor: cursor ?? this.cursor, + hasMore: hasMore ?? this.hasMore, + exception: exception ?? this.exception, + ); + } + + @override + List get props => [ + status, + headlines, + cursor, + hasMore, + exception, + ]; +} From 769861ff770c03265a019b0f23a533afc668e30e Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 11:50:17 +0100 Subject: [PATCH 28/48] build(content_management): add core dependency - Import core package in archived_headlines_bloc.dart --- .../bloc/archived_headlines/archived_headlines_bloc.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart b/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart index ccbe3445..e0052d9e 100644 --- a/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart @@ -1,4 +1,5 @@ import 'package:bloc/bloc.dart'; +import 'package:core/core.dart'; import 'package:equatable/equatable.dart'; part 'archived_headlines_event.dart'; From 7d5272d7ecd7979447db40425a78cfd9a9e3e5ea Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:03:52 +0100 Subject: [PATCH 29/48] feat(content_management): implement loading, restoring, and deleting archived headlines --- .../archived_headlines_bloc.dart | 114 +++++++++++++++++- 1 file changed, 109 insertions(+), 5 deletions(-) diff --git a/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart b/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart index e0052d9e..d2dccfa3 100644 --- a/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart @@ -1,14 +1,118 @@ import 'package:bloc/bloc.dart'; import 'package:core/core.dart'; +import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; part 'archived_headlines_event.dart'; part 'archived_headlines_state.dart'; -class ArchivedHeadlinesBloc extends Bloc { - ArchivedHeadlinesBloc() : super(ArchivedHeadlinesInitial()) { - on((event, emit) { - // TODO: implement event handler - }); +class ArchivedHeadlinesBloc + extends Bloc { + ArchivedHeadlinesBloc({ + required DataRepository headlinesRepository, + }) : _headlinesRepository = headlinesRepository, + super(const ArchivedHeadlinesState()) { + on(_onLoadArchivedHeadlinesRequested); + on(_onRestoreHeadlineRequested); + on(_onDeleteHeadlineForeverRequested); + } + + final DataRepository _headlinesRepository; + + Future _onLoadArchivedHeadlinesRequested( + LoadArchivedHeadlinesRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: ArchivedHeadlinesStatus.loading)); + try { + final isPaginating = event.startAfterId != null; + final previousHeadlines = isPaginating ? state.headlines : []; + + final paginatedHeadlines = await _headlinesRepository.readAll( + filter: {'status': ContentStatus.archived.name}, + sort: [const SortOption('updatedAt', SortOrder.desc)], + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), + ); + emit( + state.copyWith( + status: ArchivedHeadlinesStatus.success, + headlines: [...previousHeadlines, ...paginatedHeadlines.items], + cursor: paginatedHeadlines.cursor, + hasMore: paginatedHeadlines.hasMore, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + status: ArchivedHeadlinesStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: ArchivedHeadlinesStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + Future _onRestoreHeadlineRequested( + RestoreHeadlineRequested event, + Emitter emit, + ) async { + final originalHeadlines = List.from(state.headlines); + final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id); + if (headlineIndex == -1) return; + + final headlineToRestore = originalHeadlines[headlineIndex]; + final updatedHeadlines = originalHeadlines..removeAt(headlineIndex); + + emit(state.copyWith(headlines: updatedHeadlines)); + + try { + await _headlinesRepository.update( + id: event.id, + item: headlineToRestore.copyWith(status: ContentStatus.active), + ); + } on HttpException catch (e) { + emit(state.copyWith(headlines: originalHeadlines, exception: e)); + } catch (e) { + emit( + state.copyWith( + headlines: originalHeadlines, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + Future _onDeleteHeadlineForeverRequested( + DeleteHeadlineForeverRequested event, + Emitter emit, + ) async { + final originalHeadlines = List.from(state.headlines); + final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id); + if (headlineIndex == -1) return; + + final updatedHeadlines = originalHeadlines..removeAt(headlineIndex); + emit(state.copyWith(headlines: updatedHeadlines)); + + try { + await _headlinesRepository.delete(id: event.id); + } on HttpException catch (e) { + emit(state.copyWith(headlines: originalHeadlines, exception: e)); + } catch (e) { + emit( + state.copyWith( + headlines: originalHeadlines, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } } } From 7a238be9b744fe03d8eeac3b6f3b375fb91929f6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:04:03 +0100 Subject: [PATCH 30/48] feat(content_management): add ArchivedHeadlinesPage with data table and state management --- .../view/archived_headlines_page.dart | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 lib/content_management/view/archived_headlines_page.dart diff --git a/lib/content_management/view/archived_headlines_page.dart b/lib/content_management/view/archived_headlines_page.dart new file mode 100644 index 00000000..3b08b0d6 --- /dev/null +++ b/lib/content_management/view/archived_headlines_page.dart @@ -0,0 +1,200 @@ +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/content_management/bloc/archived_headlines/archived_headlines_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class ArchivedHeadlinesPage extends StatelessWidget { + const ArchivedHeadlinesPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ArchivedHeadlinesBloc( + headlinesRepository: context.read>(), + )..add(const LoadArchivedHeadlinesRequested(limit: kDefaultRowsPerPage)), + child: const _ArchivedHeadlinesView(), + ); + } +} + +class _ArchivedHeadlinesView extends StatelessWidget { + const _ArchivedHeadlinesView(); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Scaffold( + appBar: AppBar( + title: Text(l10n.archivedHeadlines), + ), + body: Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: BlocBuilder( + builder: (context, state) { + if (state.status == ArchivedHeadlinesStatus.loading && + state.headlines.isEmpty) { + return LoadingStateWidget( + icon: Icons.newspaper, + headline: l10n.loadingArchivedHeadlines, + subheadline: l10n.pleaseWait, + ); + } + + if (state.status == ArchivedHeadlinesStatus.failure) { + return FailureStateWidget( + exception: state.exception!, + onRetry: () => context.read().add( + const LoadArchivedHeadlinesRequested( + limit: kDefaultRowsPerPage, + ), + ), + ); + } + + if (state.headlines.isEmpty) { + return Center(child: Text(l10n.noArchivedHeadlinesFound)); + } + + return Column( + children: [ + if (state.status == ArchivedHeadlinesStatus.loading && + state.headlines.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.headlineTitle), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.sourceName), + size: ColumnSize.M, + ), + DataColumn2( + label: Text(l10n.lastUpdated), + size: ColumnSize.M, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + fixedWidth: 120, + ), + ], + source: _HeadlinesDataSource( + context: context, + headlines: state.headlines, + hasMore: state.hasMore, + l10n: l10n, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.headlines.length && + state.hasMore && + state.status != ArchivedHeadlinesStatus.loading) { + context.read().add( + LoadArchivedHeadlinesRequested( + startAfterId: state.cursor, + limit: kDefaultRowsPerPage, + ), + ); + } + }, + empty: Center(child: Text(l10n.noHeadlinesFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.md, + horizontalMargin: AppSpacing.md, + ), + ), + ], + ); + }, + ), + ), + ); + } +} + +class _HeadlinesDataSource extends DataTableSource { + _HeadlinesDataSource({ + required this.context, + required this.headlines, + required this.hasMore, + required this.l10n, + }); + + final BuildContext context; + final List headlines; + final bool hasMore; + final AppLocalizations l10n; + + @override + DataRow? getRow(int index) { + if (index >= headlines.length) { + return null; + } + final headline = headlines[index]; + return DataRow2( + cells: [ + DataCell( + Text( + headline.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + DataCell(Text(headline.source.name)), + DataCell( + Text( + DateFormat('dd-MM-yyyy').format(headline.updatedAt.toLocal()), + ), + ), + DataCell( + Row( + children: [ + IconButton( + icon: const Icon(Icons.restore), + tooltip: l10n.restore, + onPressed: () { + context.read().add( + RestoreHeadlineRequested(headline.id), + ); + }, + ), + IconButton( + icon: const Icon(Icons.delete_forever), + tooltip: l10n.deleteForever, + onPressed: () { + context.read().add( + DeleteHeadlineForeverRequested(headline.id), + ); + }, + ), + ], + ), + ), + ], + ); + } + + @override + bool get isRowCountApproximate => hasMore; + + @override + int get rowCount => headlines.length; + + @override + int get selectedRowCount => 0; +} From 8bc91cf5f71ab0b3d769fb2b8c11e7d35e77f134 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:09:27 +0100 Subject: [PATCH 31/48] feat(localization): add Arabic and English translations for archived headlines features --- lib/l10n/app_localizations.dart | 30 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 15 +++++++++++++++ lib/l10n/app_localizations_en.dart | 15 +++++++++++++++ lib/l10n/arb/app_ar.arb | 20 ++++++++++++++++++++ lib/l10n/arb/app_en.arb | 20 ++++++++++++++++++++ 5 files changed, 100 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ef0eb1f1..bfb3175b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1669,6 +1669,36 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Select a country'** String get countryPickerSelectCountryLabel; + + /// Title for the Archived Headlines page + /// + /// In en, this message translates to: + /// **'Archived Headlines'** + String get archivedHeadlines; + + /// Headline for loading state of archived headlines + /// + /// In en, this message translates to: + /// **'Loading Archived Headlines'** + String get loadingArchivedHeadlines; + + /// Message when no archived headlines are found + /// + /// In en, this message translates to: + /// **'No archived headlines found.'** + String get noArchivedHeadlinesFound; + + /// Tooltip for the restore button + /// + /// In en, this message translates to: + /// **'Restore'** + String get restore; + + /// Tooltip for the delete forever button + /// + /// In en, this message translates to: + /// **'Delete Forever'** + String get deleteForever; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 9aaa4b20..a1c1f816 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -880,4 +880,19 @@ class AppLocalizationsAr extends AppLocalizations { @override String get countryPickerSelectCountryLabel => 'اختر دولة'; + + @override + String get archivedHeadlines => 'العناوين المؤرشفة'; + + @override + String get loadingArchivedHeadlines => 'جاري تحميل العناوين المؤرشفة'; + + @override + String get noArchivedHeadlinesFound => 'لم يتم العثور على عناوين مؤرشفة.'; + + @override + String get restore => 'استعادة'; + + @override + String get deleteForever => 'حذف نهائي'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index d9053896..f80a2871 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -879,4 +879,19 @@ class AppLocalizationsEn extends AppLocalizations { @override String get countryPickerSelectCountryLabel => 'Select a country'; + + @override + String get archivedHeadlines => 'Archived Headlines'; + + @override + String get loadingArchivedHeadlines => 'Loading Archived Headlines'; + + @override + String get noArchivedHeadlinesFound => 'No archived headlines found.'; + + @override + String get restore => 'Restore'; + + @override + String get deleteForever => 'Delete Forever'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 795602dc..73b59930 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1093,5 +1093,25 @@ "countryPickerSelectCountryLabel": "اختر دولة", "@countryPickerSelectCountryLabel": { "description": "التسمية المعروضة عند عدم اختيار أي دولة في حقل نموذج منتقي البلد" + }, + "archivedHeadlines": "العناوين المؤرشفة", + "@archivedHeadlines": { + "description": "عنوان صفحة العناوين المؤرشفة" + }, + "loadingArchivedHeadlines": "جاري تحميل العناوين المؤرشفة", + "@loadingArchivedHeadlines": { + "description": "عنوان حالة تحميل العناوين المؤرشفة" + }, + "noArchivedHeadlinesFound": "لم يتم العثور على عناوين مؤرشفة.", + "@noArchivedHeadlinesFound": { + "description": "رسالة عند عدم العثور على عناوين مؤرشفة" + }, + "restore": "استعادة", + "@restore": { + "description": "تلميح لزر الاستعادة" + }, + "deleteForever": "حذف نهائي", + "@deleteForever": { + "description": "تلميح لزر الحذف النهائي" } } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 330be9dc..f408042c 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1093,5 +1093,25 @@ "countryPickerSelectCountryLabel": "Select a country", "@countryPickerSelectCountryLabel": { "description": "Label displayed when no country is selected in the picker form field" + }, + "archivedHeadlines": "Archived Headlines", + "@archivedHeadlines": { + "description": "Title for the Archived Headlines page" + }, + "loadingArchivedHeadlines": "Loading Archived Headlines", + "@loadingArchivedHeadlines": { + "description": "Headline for loading state of archived headlines" + }, + "noArchivedHeadlinesFound": "No archived headlines found.", + "@noArchivedHeadlinesFound": { + "description": "Message when no archived headlines are found" + }, + "restore": "Restore", + "@restore": { + "description": "Tooltip for the restore button" + }, + "deleteForever": "Delete Forever", + "@deleteForever": { + "description": "Tooltip for the delete forever button" } } From a67b062b47234d0dc953908ab6fd4ea1c63f12a8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:11:36 +0100 Subject: [PATCH 32/48] refactor(content_management): update props type in ArchivedTopicsEvent to support nullable types --- .../archived_topics_event.dart | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/content_management/bloc/archived_topics/archived_topics_event.dart b/lib/content_management/bloc/archived_topics/archived_topics_event.dart index ab77de31..b5208631 100644 --- a/lib/content_management/bloc/archived_topics/archived_topics_event.dart +++ b/lib/content_management/bloc/archived_topics/archived_topics_event.dart @@ -4,5 +4,26 @@ sealed class ArchivedTopicsEvent extends Equatable { const ArchivedTopicsEvent(); @override - List get props => []; + List get props => []; +} + +/// Event to request loading of archived topics. +final class LoadArchivedTopicsRequested extends ArchivedTopicsEvent { + const LoadArchivedTopicsRequested({this.startAfterId, this.limit}); + + final String? startAfterId; + final int? limit; + + @override + List get props => [startAfterId, limit]; +} + +/// Event to restore an archived topic. +final class RestoreTopicRequested extends ArchivedTopicsEvent { + const RestoreTopicRequested(this.id); + + final String id; + + @override + List get props => [id]; } From 1cf3e965bf50dc60d453452af9e3bc2429eec645 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:13:00 +0100 Subject: [PATCH 33/48] refactor(content_management): restructure ArchivedTopicsState to include status and additional properties --- .../archived_topics_state.dart | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/lib/content_management/bloc/archived_topics/archived_topics_state.dart b/lib/content_management/bloc/archived_topics/archived_topics_state.dart index 3e7ee029..4ac21997 100644 --- a/lib/content_management/bloc/archived_topics/archived_topics_state.dart +++ b/lib/content_management/bloc/archived_topics/archived_topics_state.dart @@ -1,10 +1,51 @@ part of 'archived_topics_bloc.dart'; -sealed class ArchivedTopicsState extends Equatable { - const ArchivedTopicsState(); - - @override - List get props => []; +/// Represents the status of archived content operations. +enum ArchivedTopicsStatus { + initial, + loading, + success, + failure, } -final class ArchivedTopicsInitial extends ArchivedTopicsState {} +/// The state for the archived content feature. +class ArchivedTopicsState extends Equatable { + const ArchivedTopicsState({ + this.status = ArchivedTopicsStatus.initial, + this.topics = const [], + this.cursor, + this.hasMore = false, + this.exception, + }); + + final ArchivedTopicsStatus status; + final List topics; + final String? cursor; + final bool hasMore; + final HttpException? exception; + + ArchivedTopicsState copyWith({ + ArchivedTopicsStatus? status, + List? topics, + String? cursor, + bool? hasMore, + HttpException? exception, + }) { + return ArchivedTopicsState( + status: status ?? this.status, + topics: topics ?? this.topics, + cursor: cursor ?? this.cursor, + hasMore: hasMore ?? this.hasMore, + exception: exception ?? this.exception, + ); + } + + @override + List get props => [ + status, + topics, + cursor, + hasMore, + exception, + ]; +} From 9772698d38f0c2239fbf75f33d60a0995a324bd0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:14:45 +0100 Subject: [PATCH 34/48] feat(content_management): enhance ArchivedTopicsBloc with pagination and restore functionality --- .../archived_topics/archived_topics_bloc.dart | 89 +++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/lib/content_management/bloc/archived_topics/archived_topics_bloc.dart b/lib/content_management/bloc/archived_topics/archived_topics_bloc.dart index ad708cc7..6cb1f198 100644 --- a/lib/content_management/bloc/archived_topics/archived_topics_bloc.dart +++ b/lib/content_management/bloc/archived_topics/archived_topics_bloc.dart @@ -1,13 +1,92 @@ import 'package:bloc/bloc.dart'; +import 'package:core/core.dart'; +import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; part 'archived_topics_event.dart'; part 'archived_topics_state.dart'; -class ArchivedTopicsBloc extends Bloc { - ArchivedTopicsBloc() : super(ArchivedTopicsInitial()) { - on((event, emit) { - // TODO: implement event handler - }); +class ArchivedTopicsBloc + extends Bloc { + ArchivedTopicsBloc({ + required DataRepository topicsRepository, + }) : _topicsRepository = topicsRepository, + super(const ArchivedTopicsState()) { + on(_onLoadArchivedTopicsRequested); + on(_onRestoreTopicRequested); + } + + final DataRepository _topicsRepository; + + Future _onLoadArchivedTopicsRequested( + LoadArchivedTopicsRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: ArchivedTopicsStatus.loading)); + try { + final isPaginating = event.startAfterId != null; + final previousTopics = isPaginating ? state.topics : []; + + final paginatedTopics = await _topicsRepository.readAll( + filter: {'status': ContentStatus.archived.name}, + sort: [const SortOption('updatedAt', SortOrder.desc)], + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), + ); + emit( + state.copyWith( + status: ArchivedTopicsStatus.success, + topics: [...previousTopics, ...paginatedTopics.items], + cursor: paginatedTopics.cursor, + hasMore: paginatedTopics.hasMore, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + status: ArchivedTopicsStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: ArchivedTopicsStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + Future _onRestoreTopicRequested( + RestoreTopicRequested event, + Emitter emit, + ) async { + final originalTopics = List.from(state.topics); + final topicIndex = originalTopics.indexWhere((t) => t.id == event.id); + if (topicIndex == -1) return; + + final topicToRestore = originalTopics[topicIndex]; + final updatedTopics = originalTopics..removeAt(topicIndex); + + emit(state.copyWith(topics: updatedTopics)); + + try { + await _topicsRepository.update( + id: event.id, + item: topicToRestore.copyWith(status: ContentStatus.active), + ); + } on HttpException catch (e) { + emit(state.copyWith(topics: originalTopics, exception: e)); + } catch (e) { + emit( + state.copyWith( + topics: originalTopics, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } } } From 97f5c366c5f0c39c02def8085ea29226f7fa7119 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:16:28 +0100 Subject: [PATCH 35/48] feat(content_management): add ArchivedTopicsPage with pagination and state management --- .../view/archived_topics_page.dart | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 lib/content_management/view/archived_topics_page.dart diff --git a/lib/content_management/view/archived_topics_page.dart b/lib/content_management/view/archived_topics_page.dart new file mode 100644 index 00000000..48eb179c --- /dev/null +++ b/lib/content_management/view/archived_topics_page.dart @@ -0,0 +1,187 @@ +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/content_management/bloc/archived_topics/archived_topics_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class ArchivedTopicsPage extends StatelessWidget { + const ArchivedTopicsPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ArchivedTopicsBloc( + topicsRepository: context.read>(), + )..add(const LoadArchivedTopicsRequested(limit: kDefaultRowsPerPage)), + child: const _ArchivedTopicsView(), + ); + } +} + +class _ArchivedTopicsView extends StatelessWidget { + const _ArchivedTopicsView(); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Scaffold( + appBar: AppBar( + title: Text(l10n.archivedTopics), //TODO(you): Localize this string + ), + body: Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: BlocBuilder( + builder: (context, state) { + if (state.status == ArchivedTopicsStatus.loading && + state.topics.isEmpty) { + return LoadingStateWidget( + icon: Icons.topic, + headline: l10n.loadingArchivedTopics, //TODO(you): Localize this string + subheadline: l10n.pleaseWait, + ); + } + + if (state.status == ArchivedTopicsStatus.failure) { + return FailureStateWidget( + exception: state.exception!, + onRetry: () => context.read().add( + const LoadArchivedTopicsRequested( + limit: kDefaultRowsPerPage, + ), + ), + ); + } + + if (state.topics.isEmpty) { + return Center(child: Text(l10n.noArchivedTopicsFound)); //TODO(you): Localize this string + } + + return Column( + children: [ + if (state.status == ArchivedTopicsStatus.loading && + state.topics.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.topicName), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.lastUpdated), + size: ColumnSize.M, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + fixedWidth: 120, + ), + ], + source: _TopicsDataSource( + context: context, + topics: state.topics, + hasMore: state.hasMore, + l10n: l10n, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.topics.length && + state.hasMore && + state.status != ArchivedTopicsStatus.loading) { + context.read().add( + LoadArchivedTopicsRequested( + startAfterId: state.cursor, + limit: kDefaultRowsPerPage, + ), + ); + } + }, + empty: Center(child: Text(l10n.noTopicsFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.md, + horizontalMargin: AppSpacing.md, + ), + ), + ], + ); + }, + ), + ), + ); + } +} + +class _TopicsDataSource extends DataTableSource { + _TopicsDataSource({ + required this.context, + required this.topics, + required this.hasMore, + required this.l10n, + }); + + final BuildContext context; + final List topics; + final bool hasMore; + final AppLocalizations l10n; + + @override + DataRow? getRow(int index) { + if (index >= topics.length) { + return null; + } + final topic = topics[index]; + return DataRow2( + cells: [ + DataCell( + Text( + topic.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + DataCell( + Text( + DateFormat('dd-MM-yyyy').format(topic.updatedAt.toLocal()), + ), + ), + DataCell( + Row( + children: [ + IconButton( + icon: const Icon(Icons.restore), + tooltip: l10n.restore, + onPressed: () { + context.read().add( + RestoreTopicRequested(topic.id), + ); + }, + ), + ], + ), + ), + ], + ); + } + + @override + bool get isRowCountApproximate => hasMore; + + @override + int get rowCount => topics.length; + + @override + int get selectedRowCount => 0; +} From daa8060197987c38ba43c0206994be9229dcebd6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:21:00 +0100 Subject: [PATCH 36/48] fix(archived_headlines): add missing import for AppLocalizations --- lib/content_management/view/archived_headlines_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/content_management/view/archived_headlines_page.dart b/lib/content_management/view/archived_headlines_page.dart index 3b08b0d6..51cfbb7b 100644 --- a/lib/content_management/view/archived_headlines_page.dart +++ b/lib/content_management/view/archived_headlines_page.dart @@ -4,6 +4,7 @@ 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/content_management/bloc/archived_headlines/archived_headlines_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; import 'package:intl/intl.dart'; From 5059e8afc747d30989c977c3f215ff44850c0ee3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:25:31 +0100 Subject: [PATCH 37/48] refactor(archived_sources): update props type in ArchivedSourcesEvent to support nullable types --- .../archived_sources_event.dart | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/content_management/bloc/archived_sources/archived_sources_event.dart b/lib/content_management/bloc/archived_sources/archived_sources_event.dart index ca3ccff3..9c77673f 100644 --- a/lib/content_management/bloc/archived_sources/archived_sources_event.dart +++ b/lib/content_management/bloc/archived_sources/archived_sources_event.dart @@ -4,5 +4,26 @@ sealed class ArchivedSourcesEvent extends Equatable { const ArchivedSourcesEvent(); @override - List get props => []; + List get props => []; +} + +/// Event to request loading of archived sources. +final class LoadArchivedSourcesRequested extends ArchivedSourcesEvent { + const LoadArchivedSourcesRequested({this.startAfterId, this.limit}); + + final String? startAfterId; + final int? limit; + + @override + List get props => [startAfterId, limit]; +} + +/// Event to restore an archived source. +final class RestoreSourceRequested extends ArchivedSourcesEvent { + const RestoreSourceRequested(this.id); + + final String id; + + @override + List get props => [id]; } From ca807500f0f3a1dcd065ac6b55e00be7d63c66a9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:25:36 +0100 Subject: [PATCH 38/48] refactor(archived_sources): redefine ArchivedSourcesState with status management and additional properties --- .../archived_sources_state.dart | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/lib/content_management/bloc/archived_sources/archived_sources_state.dart b/lib/content_management/bloc/archived_sources/archived_sources_state.dart index d542f5de..ded6188c 100644 --- a/lib/content_management/bloc/archived_sources/archived_sources_state.dart +++ b/lib/content_management/bloc/archived_sources/archived_sources_state.dart @@ -1,10 +1,51 @@ part of 'archived_sources_bloc.dart'; -sealed class ArchivedSourcesState extends Equatable { - const ArchivedSourcesState(); - - @override - List get props => []; +/// Represents the status of archived content operations. +enum ArchivedSourcesStatus { + initial, + loading, + success, + failure, } -final class ArchivedSourcesInitial extends ArchivedSourcesState {} +/// The state for the archived content feature. +class ArchivedSourcesState extends Equatable { + const ArchivedSourcesState({ + this.status = ArchivedSourcesStatus.initial, + this.sources = const [], + this.cursor, + this.hasMore = false, + this.exception, + }); + + final ArchivedSourcesStatus status; + final List sources; + final String? cursor; + final bool hasMore; + final HttpException? exception; + + ArchivedSourcesState copyWith({ + ArchivedSourcesStatus? status, + List? sources, + String? cursor, + bool? hasMore, + HttpException? exception, + }) { + return ArchivedSourcesState( + status: status ?? this.status, + sources: sources ?? this.sources, + cursor: cursor ?? this.cursor, + hasMore: hasMore ?? this.hasMore, + exception: exception ?? this.exception, + ); + } + + @override + List get props => [ + status, + sources, + cursor, + hasMore, + exception, + ]; +} From 7f0eb5f09b948c775963d380233acb7478f1c0f6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:26:34 +0100 Subject: [PATCH 39/48] refactor(archived_sources_bloc): improve error handling in source restoration process --- .../archived_sources_bloc.dart | 89 +++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/lib/content_management/bloc/archived_sources/archived_sources_bloc.dart b/lib/content_management/bloc/archived_sources/archived_sources_bloc.dart index 2d8fb0eb..6ceaf534 100644 --- a/lib/content_management/bloc/archived_sources/archived_sources_bloc.dart +++ b/lib/content_management/bloc/archived_sources/archived_sources_bloc.dart @@ -1,13 +1,92 @@ import 'package:bloc/bloc.dart'; +import 'package:core/core.dart'; +import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; part 'archived_sources_event.dart'; part 'archived_sources_state.dart'; -class ArchivedSourcesBloc extends Bloc { - ArchivedSourcesBloc() : super(ArchivedSourcesInitial()) { - on((event, emit) { - // TODO: implement event handler - }); +class ArchivedSourcesBloc + extends Bloc { + ArchivedSourcesBloc({ + required DataRepository sourcesRepository, + }) : _sourcesRepository = sourcesRepository, + super(const ArchivedSourcesState()) { + on(_onLoadArchivedSourcesRequested); + on(_onRestoreSourceRequested); + } + + final DataRepository _sourcesRepository; + + Future _onLoadArchivedSourcesRequested( + LoadArchivedSourcesRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: ArchivedSourcesStatus.loading)); + try { + final isPaginating = event.startAfterId != null; + final previousSources = isPaginating ? state.sources : []; + + final paginatedSources = await _sourcesRepository.readAll( + filter: {'status': ContentStatus.archived.name}, + sort: [const SortOption('updatedAt', SortOrder.desc)], + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), + ); + emit( + state.copyWith( + status: ArchivedSourcesStatus.success, + sources: [...previousSources, ...paginatedSources.items], + cursor: paginatedSources.cursor, + hasMore: paginatedSources.hasMore, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + status: ArchivedSourcesStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: ArchivedSourcesStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } + + Future _onRestoreSourceRequested( + RestoreSourceRequested event, + Emitter emit, + ) async { + final originalSources = List.from(state.sources); + final sourceIndex = originalSources.indexWhere((s) => s.id == event.id); + if (sourceIndex == -1) return; + + final sourceToRestore = originalSources[sourceIndex]; + final updatedSources = originalSources..removeAt(sourceIndex); + + emit(state.copyWith(sources: updatedSources)); + + try { + await _sourcesRepository.update( + id: event.id, + item: sourceToRestore.copyWith(status: ContentStatus.active), + ); + } on HttpException catch (e) { + emit(state.copyWith(sources: originalSources, exception: e)); + } catch (e) { + emit( + state.copyWith( + sources: originalSources, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } } } From da59c9c0f5b94d418c294f964dfe09151b14f209 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:28:54 +0100 Subject: [PATCH 40/48] chore: misc --- .../view/archived_headlines_page.dart | 1 - .../view/archived_sources_page.dart | 186 ++++++++++++++++++ 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 lib/content_management/view/archived_sources_page.dart diff --git a/lib/content_management/view/archived_headlines_page.dart b/lib/content_management/view/archived_headlines_page.dart index 51cfbb7b..b0886e16 100644 --- a/lib/content_management/view/archived_headlines_page.dart +++ b/lib/content_management/view/archived_headlines_page.dart @@ -6,7 +6,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/archived_headlines/archived_headlines_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; diff --git a/lib/content_management/view/archived_sources_page.dart b/lib/content_management/view/archived_sources_page.dart new file mode 100644 index 00000000..a336a294 --- /dev/null +++ b/lib/content_management/view/archived_sources_page.dart @@ -0,0 +1,186 @@ +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/content_management/bloc/archived_sources/archived_sources_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:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class ArchivedSourcesPage extends StatelessWidget { + const ArchivedSourcesPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ArchivedSourcesBloc( + sourcesRepository: context.read>(), + )..add(const LoadArchivedSourcesRequested(limit: kDefaultRowsPerPage)), + child: const _ArchivedSourcesView(), + ); + } +} + +class _ArchivedSourcesView extends StatelessWidget { + const _ArchivedSourcesView(); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Scaffold( + appBar: AppBar( + title: Text('Archived Sources'), // TODO(you): Will be fixed in l10n phase. + ), + body: Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: BlocBuilder( + builder: (context, state) { + if (state.status == ArchivedSourcesStatus.loading && + state.sources.isEmpty) { + return LoadingStateWidget( + icon: Icons.source, + headline: 'Loading Archived Sources', // TODO(you): Will be fixed in l10n phase. + subheadline: l10n.pleaseWait, + ); + } + + if (state.status == ArchivedSourcesStatus.failure) { + return FailureStateWidget( + exception: state.exception!, + onRetry: () => context.read().add( + const LoadArchivedSourcesRequested( + limit: kDefaultRowsPerPage, + ), + ), + ); + } + + if (state.sources.isEmpty) { + return Center(child: Text('No archived sources found.')); // TODO(you): Will be fixed in l10n phase. + } + + return Column( + children: [ + if (state.status == ArchivedSourcesStatus.loading && + state.sources.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.sourceName), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.lastUpdated), + size: ColumnSize.M, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + fixedWidth: 120, + ), + ], + source: _SourcesDataSource( + context: context, + sources: state.sources, + hasMore: state.hasMore, + l10n: l10n, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.sources.length && + state.hasMore && + state.status != ArchivedSourcesStatus.loading) { + context.read().add( + LoadArchivedSourcesRequested( + startAfterId: state.cursor, + limit: kDefaultRowsPerPage, + ), + ); + } + }, + empty: Center(child: Text(l10n.noSourcesFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.md, + horizontalMargin: AppSpacing.md, + ), + ), + ], + ); + }, + ), + ), + ); + } +} + +class _SourcesDataSource extends DataTableSource { + _SourcesDataSource({ + required this.context, + required this.sources, + required this.hasMore, + required this.l10n, + }); + + final BuildContext context; + final List sources; + final bool hasMore; + final AppLocalizations l10n; + + @override + DataRow? getRow(int index) { + if (index >= sources.length) { + return null; + } + final source = sources[index]; + return DataRow2( + cells: [ + DataCell( + Text( + source.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + DataCell( + Text( + DateFormat('dd-MM-yyyy').format(source.updatedAt.toLocal()), + ), + ), + DataCell( + Row( + children: [ + IconButton( + icon: const Icon(Icons.restore), + tooltip: l10n.restore, + onPressed: () { + context.read().add( + RestoreSourceRequested(source.id), + ); + }, + ), + ], + ), + ), + ], + ); + } + + @override + bool get isRowCountApproximate => hasMore; + + @override + int get rowCount => sources.length; + + @override + int get selectedRowCount => 0; +} From 13f519ea5ac493fc261652cb0110605edef829af Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:30:32 +0100 Subject: [PATCH 41/48] chore: misc --- lib/content_management/view/archived_sources_page.dart | 4 ++-- lib/content_management/view/archived_topics_page.dart | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/content_management/view/archived_sources_page.dart b/lib/content_management/view/archived_sources_page.dart index a336a294..1d869b6a 100644 --- a/lib/content_management/view/archived_sources_page.dart +++ b/lib/content_management/view/archived_sources_page.dart @@ -31,7 +31,7 @@ class _ArchivedSourcesView extends StatelessWidget { final l10n = AppLocalizationsX(context).l10n; return Scaffold( appBar: AppBar( - title: Text('Archived Sources'), // TODO(you): Will be fixed in l10n phase. + title: const Text('Archived Sources'), // TODO(you): Will be fixed in l10n phase. ), body: Padding( padding: const EdgeInsets.all(AppSpacing.lg), @@ -58,7 +58,7 @@ class _ArchivedSourcesView extends StatelessWidget { } if (state.sources.isEmpty) { - return Center(child: Text('No archived sources found.')); // TODO(you): Will be fixed in l10n phase. + return const Center(child: Text('No archived sources found.')); // TODO(you): Will be fixed in l10n phase. } return Column( diff --git a/lib/content_management/view/archived_topics_page.dart b/lib/content_management/view/archived_topics_page.dart index 48eb179c..eb2aaa7c 100644 --- a/lib/content_management/view/archived_topics_page.dart +++ b/lib/content_management/view/archived_topics_page.dart @@ -6,7 +6,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/archived_topics/archived_topics_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -32,7 +31,7 @@ class _ArchivedTopicsView extends StatelessWidget { final l10n = AppLocalizationsX(context).l10n; return Scaffold( appBar: AppBar( - title: Text(l10n.archivedTopics), //TODO(you): Localize this string + title: Text(l10n.archivedTopics), // TODO(you): Localize this string ), body: Padding( padding: const EdgeInsets.all(AppSpacing.lg), @@ -42,7 +41,7 @@ class _ArchivedTopicsView extends StatelessWidget { state.topics.isEmpty) { return LoadingStateWidget( icon: Icons.topic, - headline: l10n.loadingArchivedTopics, //TODO(you): Localize this string + headline: l10n.loadingArchivedTopics, // TODO(you): Localize this string subheadline: l10n.pleaseWait, ); } @@ -59,7 +58,7 @@ class _ArchivedTopicsView extends StatelessWidget { } if (state.topics.isEmpty) { - return Center(child: Text(l10n.noArchivedTopicsFound)); //TODO(you): Localize this string + return Center(child: Text(l10n.noArchivedTopicsFound)); // TODO(you): Localize this string } return Column( From 2799f658142a09213ddffa484d93d4df6e982a43 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:33:48 +0100 Subject: [PATCH 42/48] feat(localization): add archived topics and sources translations --- lib/l10n/app_localizations.dart | 54 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 27 +++++++++++++++ lib/l10n/app_localizations_en.dart | 27 +++++++++++++++ lib/l10n/arb/app_ar.arb | 36 ++++++++++++++++++++ lib/l10n/arb/app_en.arb | 36 ++++++++++++++++++++ 5 files changed, 180 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bfb3175b..56a92759 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1699,6 +1699,60 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Delete Forever'** String get deleteForever; + + /// Title for the Archived Topics page + /// + /// In en, this message translates to: + /// **'Archived Topics'** + String get archivedTopics; + + /// Headline for loading state of archived topics + /// + /// In en, this message translates to: + /// **'Loading Archived Topics'** + String get loadingArchivedTopics; + + /// Message when no archived topics are found + /// + /// In en, this message translates to: + /// **'No archived topics found.'** + String get noArchivedTopicsFound; + + /// Title for the Archived Sources page + /// + /// In en, this message translates to: + /// **'Archived Sources'** + String get archivedSources; + + /// Headline for loading state of archived sources + /// + /// In en, this message translates to: + /// **'Loading Archived Sources'** + String get loadingArchivedSources; + + /// Message when no archived sources are found + /// + /// In en, this message translates to: + /// **'No archived sources found.'** + String get noArchivedSourcesFound; + + /// Tooltip for the archived items button + /// + /// In en, this message translates to: + /// **'Archived Items'** + String get archivedItems; + + /// Tooltip for the add new item button + /// + /// In en, this message translates to: + /// **'Add New Item'** + String get addNewItem; + + /// Tooltip for the archive button + /// + /// In en, this message translates to: + /// **'Archive'** + String get archive; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index a1c1f816..1583d687 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -895,4 +895,31 @@ class AppLocalizationsAr extends AppLocalizations { @override String get deleteForever => 'حذف نهائي'; + + @override + String get archivedTopics => 'المواضيع المؤرشفة'; + + @override + String get loadingArchivedTopics => 'جاري تحميل المواضيع المؤرشفة'; + + @override + String get noArchivedTopicsFound => 'لم يتم العثور على مواضيع مؤرشفة.'; + + @override + String get archivedSources => 'المصادر المؤرشفة'; + + @override + String get loadingArchivedSources => 'جاري تحميل المصادر المؤرشفة'; + + @override + String get noArchivedSourcesFound => 'لم يتم العثور على مصادر مؤرشفة.'; + + @override + String get archivedItems => 'العناصر المؤرشفة'; + + @override + String get addNewItem => 'إضافة عنصر جديد'; + + @override + String get archive => 'أرشفة'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f80a2871..77a372a3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -894,4 +894,31 @@ class AppLocalizationsEn extends AppLocalizations { @override String get deleteForever => 'Delete Forever'; + + @override + String get archivedTopics => 'Archived Topics'; + + @override + String get loadingArchivedTopics => 'Loading Archived Topics'; + + @override + String get noArchivedTopicsFound => 'No archived topics found.'; + + @override + String get archivedSources => 'Archived Sources'; + + @override + String get loadingArchivedSources => 'Loading Archived Sources'; + + @override + String get noArchivedSourcesFound => 'No archived sources found.'; + + @override + String get archivedItems => 'Archived Items'; + + @override + String get addNewItem => 'Add New Item'; + + @override + String get archive => 'Archive'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 73b59930..71d801b8 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1113,5 +1113,41 @@ "deleteForever": "حذف نهائي", "@deleteForever": { "description": "تلميح لزر الحذف النهائي" + }, + "archivedTopics": "المواضيع المؤرشفة", + "@archivedTopics": { + "description": "عنوان صفحة المواضيع المؤرشفة" + }, + "loadingArchivedTopics": "جاري تحميل المواضيع المؤرشفة", + "@loadingArchivedTopics": { + "description": "عنوان حالة تحميل المواضيع المؤرشفة" + }, + "noArchivedTopicsFound": "لم يتم العثور على مواضيع مؤرشفة.", + "@noArchivedTopicsFound": { + "description": "رسالة عند عدم العثور على مواضيع مؤرشفة" + }, + "archivedSources": "المصادر المؤرشفة", + "@archivedSources": { + "description": "عنوان صفحة المصادر المؤرشفة" + }, + "loadingArchivedSources": "جاري تحميل المصادر المؤرشفة", + "@loadingArchivedSources": { + "description": "عنوان حالة تحميل المصادر المؤرشفة" + }, + "noArchivedSourcesFound": "لم يتم العثور على مصادر مؤرشفة.", + "@noArchivedSourcesFound": { + "description": "رسالة عند عدم العثور على مصادر مؤرشفة" + }, + "archivedItems": "العناصر المؤرشفة", + "@archivedItems": { + "description": "تلميح لزر العناصر المؤرشفة" + }, + "addNewItem": "إضافة عنصر جديد", + "@addNewItem": { + "description": "تلميح لزر إضافة عنصر جديد" + }, + "archive": "أرشفة", + "@archive": { + "description": "تلميح لزر الأرشفة" } } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index f408042c..2c1a93f3 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1113,5 +1113,41 @@ "deleteForever": "Delete Forever", "@deleteForever": { "description": "Tooltip for the delete forever button" + }, + "archivedTopics": "Archived Topics", + "@archivedTopics": { + "description": "Title for the Archived Topics page" + }, + "loadingArchivedTopics": "Loading Archived Topics", + "@loadingArchivedTopics": { + "description": "Headline for loading state of archived topics" + }, + "noArchivedTopicsFound": "No archived topics found.", + "@noArchivedTopicsFound": { + "description": "Message when no archived topics are found" + }, + "archivedSources": "Archived Sources", + "@archivedSources": { + "description": "Title for the Archived Sources page" + }, + "loadingArchivedSources": "Loading Archived Sources", + "@loadingArchivedSources": { + "description": "Headline for loading state of archived sources" + }, + "noArchivedSourcesFound": "No archived sources found.", + "@noArchivedSourcesFound": { + "description": "Message when no archived sources are found" + }, + "archivedItems": "Archived Items", + "@archivedItems": { + "description": "Tooltip for the archived items button" + }, + "addNewItem": "Add New Item", + "@addNewItem": { + "description": "Tooltip for the add new item button" + }, + "archive": "Archive", + "@archive": { + "description": "Tooltip for the archive button" } } From 72ebd2150b8858045736cd2124482eee3d9ed193 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:40:04 +0100 Subject: [PATCH 43/48] fix(localization): remove TODO comments for localization in archived topics view --- lib/content_management/view/archived_topics_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/content_management/view/archived_topics_page.dart b/lib/content_management/view/archived_topics_page.dart index eb2aaa7c..a86ff729 100644 --- a/lib/content_management/view/archived_topics_page.dart +++ b/lib/content_management/view/archived_topics_page.dart @@ -31,7 +31,7 @@ class _ArchivedTopicsView extends StatelessWidget { final l10n = AppLocalizationsX(context).l10n; return Scaffold( appBar: AppBar( - title: Text(l10n.archivedTopics), // TODO(you): Localize this string + title: Text(l10n.archivedTopics), ), body: Padding( padding: const EdgeInsets.all(AppSpacing.lg), @@ -41,7 +41,7 @@ class _ArchivedTopicsView extends StatelessWidget { state.topics.isEmpty) { return LoadingStateWidget( icon: Icons.topic, - headline: l10n.loadingArchivedTopics, // TODO(you): Localize this string + headline: l10n.loadingArchivedTopics, subheadline: l10n.pleaseWait, ); } @@ -58,7 +58,7 @@ class _ArchivedTopicsView extends StatelessWidget { } if (state.topics.isEmpty) { - return Center(child: Text(l10n.noArchivedTopicsFound)); // TODO(you): Localize this string + return Center(child: Text(l10n.noArchivedTopicsFound)); } return Column( From 66223fea8e47eb8ffe180ce4f1aaa6f78e8fd458 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:40:25 +0100 Subject: [PATCH 44/48] fix(localization): replace hardcoded strings with localized values in archived sources view --- lib/content_management/view/archived_sources_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/content_management/view/archived_sources_page.dart b/lib/content_management/view/archived_sources_page.dart index 1d869b6a..1a56338c 100644 --- a/lib/content_management/view/archived_sources_page.dart +++ b/lib/content_management/view/archived_sources_page.dart @@ -31,7 +31,7 @@ class _ArchivedSourcesView extends StatelessWidget { final l10n = AppLocalizationsX(context).l10n; return Scaffold( appBar: AppBar( - title: const Text('Archived Sources'), // TODO(you): Will be fixed in l10n phase. + title: Text(l10n.archivedSources), ), body: Padding( padding: const EdgeInsets.all(AppSpacing.lg), @@ -41,7 +41,7 @@ class _ArchivedSourcesView extends StatelessWidget { state.sources.isEmpty) { return LoadingStateWidget( icon: Icons.source, - headline: 'Loading Archived Sources', // TODO(you): Will be fixed in l10n phase. + headline: l10n.loadingArchivedSources, subheadline: l10n.pleaseWait, ); } @@ -58,7 +58,7 @@ class _ArchivedSourcesView extends StatelessWidget { } if (state.sources.isEmpty) { - return const Center(child: Text('No archived sources found.')); // TODO(you): Will be fixed in l10n phase. + return Center(child: Text(l10n.noArchivedSourcesFound)); } return Column( From 95536d241500651a87952d018a5f56de148aab44 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:41:00 +0100 Subject: [PATCH 45/48] fix(localization): replace hardcoded tooltips with localized values in content management page --- lib/content_management/view/content_management_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/content_management/view/content_management_page.dart b/lib/content_management/view/content_management_page.dart index 7a065052..1ded64f5 100644 --- a/lib/content_management/view/content_management_page.dart +++ b/lib/content_management/view/content_management_page.dart @@ -94,7 +94,7 @@ class _ContentManagementPageState extends State actions: [ IconButton( icon: const Icon(Icons.inventory_2_outlined), - tooltip: 'Archived Items', // TODO(you): Will be fixed in l10n phase. + tooltip: l10n.archivedItems, onPressed: () { final currentTab = context.read().state.activeTab; @@ -110,7 +110,7 @@ class _ContentManagementPageState extends State ), IconButton( icon: const Icon(Icons.add), - tooltip: 'Add New Item', // TODO(you): Will be fixed in l10n phase. + tooltip: l10n.addNewItem, onPressed: () { final currentTab = context .read() From 1f3c83caa21c331e6414bdea64ed25515d788525 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:49:04 +0100 Subject: [PATCH 46/48] fix(localization): replace hardcoded tooltip with localized value in headlines data source --- lib/content_management/view/headlines_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index a2a9e3b1..fd847507 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -186,7 +186,7 @@ class _HeadlinesDataSource extends DataTableSource { ), IconButton( icon: const Icon(Icons.archive), - tooltip: 'Archive', // TODO(you): Will be fixed in l10n phase. + tooltip: l10n.archive, onPressed: () { context.read().add( ArchiveHeadlineRequested(headline.id), From 157077617a00fa7aea3c1b564b6ab5deacf900a7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:49:54 +0100 Subject: [PATCH 47/48] fix(localization): replace hardcoded tooltip with localized value in sources data source --- lib/content_management/view/sources_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index a78af66a..9c14cc87 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -187,7 +187,7 @@ class _SourcesDataSource extends DataTableSource { ), IconButton( icon: const Icon(Icons.archive), - tooltip: 'Archive', // TODO(you): Will be fixed in l10n phase. + tooltip: l10n.archive, onPressed: () { // Dispatch delete event context.read().add( From 600be46df74cdfec08283cdcb9dcd57443952a24 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 12:50:31 +0100 Subject: [PATCH 48/48] fix(localization): replace hardcoded tooltip with localized value for archive action in topics data source --- lib/content_management/view/topics_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/content_management/view/topics_page.dart b/lib/content_management/view/topics_page.dart index 374d310b..c9b64393 100644 --- a/lib/content_management/view/topics_page.dart +++ b/lib/content_management/view/topics_page.dart @@ -180,7 +180,7 @@ class _TopicsDataSource extends DataTableSource { ), IconButton( icon: const Icon(Icons.archive), - tooltip: 'Archive', // TODO(you): Will be fixed in l10n phase. + tooltip: l10n.archive, onPressed: () { // Dispatch delete event context.read().add(