From 718b1e76cd27a044082786010fa10d270246fd19 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 18:47:52 +0100 Subject: [PATCH 1/9] feat(content_management): add shared data loading functionality - Implement loading of countries and languages in ContentManagementBloc - Add SharedDataRequested event - Extend ContentManagementState to include countries and languages data - Implement fetchAll helper function for efficient data retrieval - Add error handling for data loading operations --- .../bloc/content_management_bloc.dart | 101 +++++++++++++++++- .../bloc/content_management_event.dart | 9 ++ .../bloc/content_management_state.dart | 28 +++++ 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index 8f860ac1..e5958afd 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -24,10 +24,15 @@ class ContentManagementBloc required DataRepository headlinesRepository, required DataRepository topicsRepository, required DataRepository sourcesRepository, - }) : _headlinesRepository = headlinesRepository, - _topicsRepository = topicsRepository, - _sourcesRepository = sourcesRepository, - super(const ContentManagementState()) { + required DataRepository countriesRepository, + required DataRepository languagesRepository, + }) : _headlinesRepository = headlinesRepository, + _topicsRepository = topicsRepository, + _sourcesRepository = sourcesRepository, + _countriesRepository = countriesRepository, + _languagesRepository = languagesRepository, + super(const ContentManagementState()) { + on(_onSharedDataRequested); on(_onContentManagementTabChanged); on(_onLoadHeadlinesRequested); on(_onHeadlineUpdated); @@ -43,6 +48,94 @@ class ContentManagementBloc final DataRepository _headlinesRepository; final DataRepository _topicsRepository; final DataRepository _sourcesRepository; + final DataRepository _countriesRepository; + final DataRepository _languagesRepository; + + Future _onSharedDataRequested( + SharedDataRequested event, + Emitter emit, + ) async { + // Helper function to fetch all items of a given type. + Future> fetchAll({ + required DataRepository repository, + required List sort, + }) async { + final allItems = []; + String? cursor; + bool hasMore; + + do { + final response = await repository.readAll( + sort: sort, + pagination: PaginationOptions(cursor: cursor), + filter: {'status': ContentStatus.active.name}, + ); + allItems.addAll(response.items); + cursor = response.cursor; + hasMore = response.hasMore; + } while (hasMore); + + return allItems; + } + + // Check if data is already loaded or is currently loading to prevent + // redundant fetches. + if (state.allCountriesStatus == ContentManagementStatus.success && + state.allLanguagesStatus == ContentManagementStatus.success) { + return; + } + + // Set loading status for both lists. + emit( + state.copyWith( + allCountriesStatus: ContentManagementStatus.loading, + allLanguagesStatus: ContentManagementStatus.loading, + ), + ); + + try { + // Fetch both lists in parallel. + final results = await Future.wait([ + fetchAll( + repository: _countriesRepository, + sort: [const SortOption('name', SortOrder.asc)], + ), + fetchAll( + repository: _languagesRepository, + sort: [const SortOption('name', SortOrder.asc)], + ), + ]); + + final countries = results[0] as List; + final languages = results[1] as List; + + // Update the state with the complete lists. + emit( + state.copyWith( + allCountries: countries, + allCountriesStatus: ContentManagementStatus.success, + allLanguages: languages, + allLanguagesStatus: ContentManagementStatus.success, + ), + ); + } on HttpException catch (e) { + emit( + state.copyWith( + allCountriesStatus: ContentManagementStatus.failure, + allLanguagesStatus: ContentManagementStatus.failure, + exception: e, + ), + ); + } catch (e) { + emit( + state.copyWith( + allCountriesStatus: ContentManagementStatus.failure, + allLanguagesStatus: ContentManagementStatus.failure, + exception: UnknownException('An unexpected error occurred: $e'), + ), + ); + } + } void _onContentManagementTabChanged( ContentManagementTabChanged event, diff --git a/lib/content_management/bloc/content_management_event.dart b/lib/content_management/bloc/content_management_event.dart index 3e1d95b2..23711dde 100644 --- a/lib/content_management/bloc/content_management_event.dart +++ b/lib/content_management/bloc/content_management_event.dart @@ -155,3 +155,12 @@ final class SourceUpdated extends ContentManagementEvent { @override List get props => [source]; } + +/// {@template shared_data_requested} +/// Event to request loading of shared data like countries and languages. +/// This should be dispatched once when the content management section is loaded. +/// {@endtemplate} +final class SharedDataRequested extends ContentManagementEvent { + /// {@macro shared_data_requested} + const SharedDataRequested(); +} diff --git a/lib/content_management/bloc/content_management_state.dart b/lib/content_management/bloc/content_management_state.dart index d71a388f..71afbc2e 100644 --- a/lib/content_management/bloc/content_management_state.dart +++ b/lib/content_management/bloc/content_management_state.dart @@ -32,6 +32,10 @@ class ContentManagementState extends Equatable { this.sources = const [], this.sourcesCursor, this.sourcesHasMore = false, + this.allCountriesStatus = ContentManagementStatus.initial, + this.allCountries = const [], + this.allLanguagesStatus = ContentManagementStatus.initial, + this.allLanguages = const [], this.exception, }); @@ -74,6 +78,18 @@ class ContentManagementState extends Equatable { /// Indicates if there are more sources to load. final bool sourcesHasMore; + /// Status of all countries data operations. + final ContentManagementStatus allCountriesStatus; + + /// Cached list of all countries. + final List allCountries; + + /// Status of all languages data operations. + final ContentManagementStatus allLanguagesStatus; + + /// Cached list of all languages. + final List allLanguages; + /// The error describing an operation failure, if any. final HttpException? exception; @@ -92,6 +108,10 @@ class ContentManagementState extends Equatable { List? sources, String? sourcesCursor, bool? sourcesHasMore, + ContentManagementStatus? allCountriesStatus, + List? allCountries, + ContentManagementStatus? allLanguagesStatus, + List? allLanguages, HttpException? exception, }) { return ContentManagementState( @@ -108,6 +128,10 @@ class ContentManagementState extends Equatable { sources: sources ?? this.sources, sourcesCursor: sourcesCursor ?? this.sourcesCursor, sourcesHasMore: sourcesHasMore ?? this.sourcesHasMore, + allCountriesStatus: allCountriesStatus ?? this.allCountriesStatus, + allCountries: allCountries ?? this.allCountries, + allLanguagesStatus: allLanguagesStatus ?? this.allLanguagesStatus, + allLanguages: allLanguages ?? this.allLanguages, exception: exception ?? this.exception, ); } @@ -127,6 +151,10 @@ class ContentManagementState extends Equatable { sources, sourcesCursor, sourcesHasMore, + allCountriesStatus, + allCountries, + allLanguagesStatus, + allLanguages, exception, ]; } From cdc1b4c3153a9df31f687e3c5fc07b23948b5477 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 18:48:07 +0100 Subject: [PATCH 2/9] feat(SharedDataBloc): add country and language repositories and trigger data request - Inject countriesRepository and languagesRepository into SharedDataBloc - Dispatch SharedDataRequested event after bloc initialization - Update dependencies in App widget --- lib/app/view/app.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 34fdb65f..f47f208c 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -108,7 +108,9 @@ class App extends StatelessWidget { headlinesRepository: context.read>(), topicsRepository: context.read>(), sourcesRepository: context.read>(), - ), + countriesRepository: context.read>(), + languagesRepository: context.read>(), + )..add(const SharedDataRequested()), ), BlocProvider( create: (context) => DashboardBloc( From c6f1e5134c1536305d8e838d643dd6b5c55f14e5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 18:51:54 +0100 Subject: [PATCH 3/9] refactor(content_management): simplify country dropdown pagination - Remove background fetching mechanism for countries list - Load all countries at once in CreateHeadlineBloc initialization - Remove unnecessary country-related state properties from CreateHeadlineState - Update CreateHeadlinePage to use pre-loaded countries list - Remove _FetchNextCountryPage event and related handling --- .../create_headline/create_headline_bloc.dart | 73 ++----------------- .../create_headline_state.dart | 16 ---- .../view/create_headline_page.dart | 17 ++--- 3 files changed, 13 insertions(+), 93 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index 68655f5c..aa409243 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -8,10 +8,6 @@ import 'package:uuid/uuid.dart'; part 'create_headline_event.dart'; part 'create_headline_state.dart'; -final class _FetchNextCountryPage extends CreateHeadlineEvent { - const _FetchNextCountryPage(); -} - /// A BLoC to manage the state of creating a new headline. class CreateHeadlineBloc extends Bloc { @@ -20,12 +16,11 @@ class CreateHeadlineBloc required DataRepository headlinesRepository, required DataRepository sourcesRepository, required DataRepository topicsRepository, - required DataRepository countriesRepository, - }) : _headlinesRepository = headlinesRepository, - _sourcesRepository = sourcesRepository, - _topicsRepository = topicsRepository, - _countriesRepository = countriesRepository, - super(const CreateHeadlineState()) { + required List countries, + }) : _headlinesRepository = headlinesRepository, + _sourcesRepository = sourcesRepository, + _topicsRepository = topicsRepository, + super(CreateHeadlineState(countries: countries)) { on(_onDataLoaded); on(_onTitleChanged); on(_onExcerptChanged); @@ -36,13 +31,11 @@ class CreateHeadlineBloc on(_onCountryChanged); on(_onStatusChanged); on(_onSubmitted); - on<_FetchNextCountryPage>(_onFetchNextCountryPage); } final DataRepository _headlinesRepository; final DataRepository _sourcesRepository; final DataRepository _topicsRepository; - final DataRepository _countriesRepository; final _uuid = const Uuid(); Future _onDataLoaded( @@ -65,26 +58,13 @@ class CreateHeadlineBloc final sources = (sourcesResponse as PaginatedResponse).items; final topics = (topicsResponse as PaginatedResponse).items; - final countriesResponse = await _countriesRepository.readAll( - sort: [const SortOption('name', SortOrder.asc)], - ); - emit( state.copyWith( status: CreateHeadlineStatus.initial, sources: sources, topics: topics, - countries: countriesResponse.items, - countriesCursor: countriesResponse.cursor, - countriesHasMore: countriesResponse.hasMore, ), ); - - // After the initial page of countries is loaded, start a background - // process to fetch all remaining pages. - if (state.countriesHasMore) { - add(const _FetchNextCountryPage()); - } } on HttpException catch (e) { emit(state.copyWith(status: CreateHeadlineStatus.failure, exception: e)); } catch (e) { @@ -159,49 +139,6 @@ class CreateHeadlineBloc } // --- Background Data Fetching for Dropdown --- - // The DropdownButtonFormField widget does not natively support on-scroll - // pagination. To preserve UI consistency across the application, this BLoC - // employs an event-driven background fetching mechanism. - // - // After the first page of items is loaded, a chain of events is initiated - // to progressively fetch all remaining pages. This process is throttled - // and runs in the background, ensuring the UI remains responsive while the - // full list of dropdown options is populated over time. - Future _onFetchNextCountryPage( - _FetchNextCountryPage event, - Emitter emit, - ) async { - if (!state.countriesHasMore || state.countriesIsLoadingMore) return; - - try { - emit(state.copyWith(countriesIsLoadingMore: true)); - - // ignore: inference_failure_on_instance_creation - await Future.delayed(const Duration(milliseconds: 400)); - - final nextCountries = await _countriesRepository.readAll( - pagination: PaginationOptions(cursor: state.countriesCursor), - sort: [const SortOption('name', SortOrder.asc)], - ); - - emit( - state.copyWith( - countries: List.of(state.countries)..addAll(nextCountries.items), - countriesCursor: nextCountries.cursor, - countriesHasMore: nextCountries.hasMore, - countriesIsLoadingMore: false, - ), - ); - - if (nextCountries.hasMore) { - add(const _FetchNextCountryPage()); - } - } catch (e) { - emit(state.copyWith(countriesIsLoadingMore: false)); - // Optionally log the error without disrupting the user - } - } - Future _onSubmitted( CreateHeadlineSubmitted event, Emitter emit, diff --git a/lib/content_management/bloc/create_headline/create_headline_state.dart b/lib/content_management/bloc/create_headline/create_headline_state.dart index 1fb3f0c9..0739db1c 100644 --- a/lib/content_management/bloc/create_headline/create_headline_state.dart +++ b/lib/content_management/bloc/create_headline/create_headline_state.dart @@ -32,9 +32,6 @@ final class CreateHeadlineState extends Equatable { this.sources = const [], this.topics = const [], this.countries = const [], - this.countriesHasMore = true, - this.countriesIsLoadingMore = false, - this.countriesCursor, this.contentStatus = ContentStatus.active, this.exception, this.createdHeadline, @@ -51,9 +48,6 @@ final class CreateHeadlineState extends Equatable { final List sources; final List topics; final List countries; - final bool countriesHasMore; - final bool countriesIsLoadingMore; - final String? countriesCursor; final ContentStatus contentStatus; final HttpException? exception; final Headline? createdHeadline; @@ -80,9 +74,6 @@ final class CreateHeadlineState extends Equatable { List? sources, List? topics, List? countries, - bool? countriesHasMore, - bool? countriesIsLoadingMore, - String? countriesCursor, ContentStatus? contentStatus, HttpException? exception, Headline? createdHeadline, @@ -99,10 +90,6 @@ final class CreateHeadlineState extends Equatable { sources: sources ?? this.sources, topics: topics ?? this.topics, countries: countries ?? this.countries, - countriesHasMore: countriesHasMore ?? this.countriesHasMore, - countriesIsLoadingMore: - countriesIsLoadingMore ?? this.countriesIsLoadingMore, - countriesCursor: countriesCursor ?? this.countriesCursor, contentStatus: contentStatus ?? this.contentStatus, exception: exception, createdHeadline: createdHeadline ?? this.createdHeadline, @@ -122,9 +109,6 @@ final class CreateHeadlineState extends Equatable { sources, topics, countries, - countriesHasMore, - countriesIsLoadingMore, - countriesCursor, contentStatus, exception, createdHeadline, diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index ecc66f8f..36b0b572 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -20,12 +20,16 @@ class CreateHeadlinePage extends StatelessWidget { @override Widget build(BuildContext context) { + // The list of all countries is fetched once and cached in the + // ContentManagementBloc. We read it here and provide it to the + // CreateHeadlineBloc. + final allCountries = context.read().state.allCountries; return BlocProvider( create: (context) => CreateHeadlineBloc( headlinesRepository: context.read>(), sourcesRepository: context.read>(), topicsRepository: context.read>(), - countriesRepository: context.read>(), + countries: allCountries, )..add(const CreateHeadlineDataLoaded()), child: const _CreateHeadlineView(), ); @@ -220,9 +224,6 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { decoration: InputDecoration( labelText: l10n.countryName, border: const OutlineInputBorder(), - helperText: state.countriesIsLoadingMore - ? l10n.loadingFullList - : null, ), items: [ DropdownMenuItem(value: null, child: Text(l10n.none)), @@ -249,11 +250,9 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { ), ), ], - onChanged: state.countriesIsLoadingMore - ? null - : (value) => context.read().add( - CreateHeadlineCountryChanged(value), - ), + onChanged: (value) => context + .read() + .add(CreateHeadlineCountryChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( From e495b1f332e628d36c7703a86f1ea37be0858ee9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 18:53:56 +0100 Subject: [PATCH 4/9] refactor(content_management): simplify country dropdown pagination - Remove background fetching mechanism for countries list - Load all countries at once in EditHeadlineBloc initialization - Update EditHeadlineState to remove pagination-related variables - Modify EditHeadlinePage to use a predefined list of countries - Remove loading indicator and related logic from the dropdown --- .../edit_headline/edit_headline_bloc.dart | 75 ++----------------- .../edit_headline/edit_headline_state.dart | 16 ---- .../view/edit_headline_page.dart | 18 ++--- 3 files changed, 15 insertions(+), 94 deletions(-) diff --git a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart index 788bba18..10f398f8 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart @@ -7,10 +7,6 @@ import 'package:flutter/foundation.dart'; part 'edit_headline_event.dart'; part 'edit_headline_state.dart'; -final class _FetchNextCountryPage extends EditHeadlineEvent { - const _FetchNextCountryPage(); -} - /// A BLoC to manage the state of editing a single headline. class EditHeadlineBloc extends Bloc { /// {@macro edit_headline_bloc} @@ -18,14 +14,13 @@ class EditHeadlineBloc extends Bloc { required DataRepository headlinesRepository, required DataRepository sourcesRepository, required DataRepository topicsRepository, - required DataRepository countriesRepository, + required List countries, required String headlineId, - }) : _headlinesRepository = headlinesRepository, - _sourcesRepository = sourcesRepository, - _topicsRepository = topicsRepository, - _countriesRepository = countriesRepository, - _headlineId = headlineId, - super(const EditHeadlineState()) { + }) : _headlinesRepository = headlinesRepository, + _sourcesRepository = sourcesRepository, + _topicsRepository = topicsRepository, + _headlineId = headlineId, + super(EditHeadlineState(countries: countries)) { on(_onLoaded); on(_onTitleChanged); on(_onExcerptChanged); @@ -36,13 +31,11 @@ class EditHeadlineBloc extends Bloc { on(_onCountryChanged); on(_onStatusChanged); on(_onSubmitted); - on<_FetchNextCountryPage>(_onFetchNextCountryPage); } final DataRepository _headlinesRepository; final DataRepository _sourcesRepository; final DataRepository _topicsRepository; - final DataRepository _countriesRepository; final String _headlineId; Future _onLoaded( @@ -67,10 +60,6 @@ class EditHeadlineBloc extends Bloc { final sources = (responses[1] as PaginatedResponse).items; final topics = (responses[2] as PaginatedResponse).items; - final countriesResponse = await _countriesRepository.readAll( - sort: [const SortOption('name', SortOrder.asc)], - ); - emit( state.copyWith( status: EditHeadlineStatus.initial, @@ -84,18 +73,9 @@ class EditHeadlineBloc extends Bloc { eventCountry: () => headline.eventCountry, sources: sources, topics: topics, - countries: countriesResponse.items, - countriesCursor: countriesResponse.cursor, - countriesHasMore: countriesResponse.hasMore, contentStatus: headline.status, ), ); - - // After the initial page of countries is loaded, start a background - // process to fetch all remaining pages. - if (state.countriesHasMore) { - add(const _FetchNextCountryPage()); - } } on HttpException catch (e) { emit(state.copyWith(status: EditHeadlineStatus.failure, exception: e)); } catch (e) { @@ -197,49 +177,6 @@ class EditHeadlineBloc extends Bloc { } // --- Background Data Fetching for Dropdown --- - // The DropdownButtonFormField widget does not natively support on-scroll - // pagination. To preserve UI consistency across the application, this BLoC - // employs an event-driven background fetching mechanism. - // - // After the first page of items is loaded, a chain of events is initiated - // to progressively fetch all remaining pages. This process is throttled - // and runs in the background, ensuring the UI remains responsive while the - // full list of dropdown options is populated over time. - Future _onFetchNextCountryPage( - _FetchNextCountryPage event, - Emitter emit, - ) async { - if (!state.countriesHasMore || state.countriesIsLoadingMore) return; - - try { - emit(state.copyWith(countriesIsLoadingMore: true)); - - // ignore: inference_failure_on_instance_creation - await Future.delayed(const Duration(milliseconds: 400)); - - final nextCountries = await _countriesRepository.readAll( - pagination: PaginationOptions(cursor: state.countriesCursor), - sort: [const SortOption('name', SortOrder.asc)], - ); - - emit( - state.copyWith( - countries: List.of(state.countries)..addAll(nextCountries.items), - countriesCursor: nextCountries.cursor, - countriesHasMore: nextCountries.hasMore, - countriesIsLoadingMore: false, - ), - ); - - if (nextCountries.hasMore) { - add(const _FetchNextCountryPage()); - } - } catch (e) { - emit(state.copyWith(countriesIsLoadingMore: false)); - // Optionally log the error without disrupting the user - } - } - Future _onSubmitted( EditHeadlineSubmitted event, Emitter emit, diff --git a/lib/content_management/bloc/edit_headline/edit_headline_state.dart b/lib/content_management/bloc/edit_headline/edit_headline_state.dart index 30688de8..fba983f4 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_state.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_state.dart @@ -33,9 +33,6 @@ final class EditHeadlineState extends Equatable { this.sources = const [], this.topics = const [], this.countries = const [], - this.countriesHasMore = true, - this.countriesIsLoadingMore = false, - this.countriesCursor, this.contentStatus = ContentStatus.active, this.exception, this.updatedHeadline, @@ -53,9 +50,6 @@ final class EditHeadlineState extends Equatable { final List sources; final List topics; final List countries; - final bool countriesHasMore; - final bool countriesIsLoadingMore; - final String? countriesCursor; final ContentStatus contentStatus; final HttpException? exception; final Headline? updatedHeadline; @@ -83,9 +77,6 @@ final class EditHeadlineState extends Equatable { List? sources, List? topics, List? countries, - bool? countriesHasMore, - bool? countriesIsLoadingMore, - String? countriesCursor, ContentStatus? contentStatus, HttpException? exception, Headline? updatedHeadline, @@ -103,10 +94,6 @@ final class EditHeadlineState extends Equatable { sources: sources ?? this.sources, topics: topics ?? this.topics, countries: countries ?? this.countries, - countriesHasMore: countriesHasMore ?? this.countriesHasMore, - countriesIsLoadingMore: - countriesIsLoadingMore ?? this.countriesIsLoadingMore, - countriesCursor: countriesCursor ?? this.countriesCursor, contentStatus: contentStatus ?? this.contentStatus, exception: exception, updatedHeadline: updatedHeadline ?? this.updatedHeadline, @@ -127,9 +114,6 @@ final class EditHeadlineState extends Equatable { sources, topics, countries, - countriesHasMore, - countriesIsLoadingMore, - countriesCursor, contentStatus, exception, updatedHeadline, diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index 2c7dd21f..49bcbf36 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -23,12 +23,17 @@ class EditHeadlinePage extends StatelessWidget { @override Widget build(BuildContext context) { + // The list of all countries is fetched once and cached in the + // ContentManagementBloc. We read it here and provide it to the + // EditHeadlineBloc. + final allCountries = + context.read().state.allCountries; return BlocProvider( create: (context) => EditHeadlineBloc( headlinesRepository: context.read>(), sourcesRepository: context.read>(), topicsRepository: context.read>(), - countriesRepository: context.read>(), + countries: allCountries, headlineId: headlineId, )..add(const EditHeadlineLoaded()), child: const _EditHeadlineView(), @@ -288,9 +293,6 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { decoration: InputDecoration( labelText: l10n.countryName, border: const OutlineInputBorder(), - helperText: state.countriesIsLoadingMore - ? l10n.loadingFullList - : null, ), items: [ DropdownMenuItem(value: null, child: Text(l10n.none)), @@ -317,11 +319,9 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { ), ), ], - onChanged: state.countriesIsLoadingMore - ? null - : (value) => context.read().add( - EditHeadlineCountryChanged(value), - ), + onChanged: (value) => context + .read() + .add(EditHeadlineCountryChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( From 5f10e99494d9fd1c32f618c5c6bd8c1e21c81a81 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 19:04:31 +0100 Subject: [PATCH 5/9] refactor(content_management): simplify source creation form data handling - Remove pagination and background fetching for countries and languages - Load full lists of countries and languages at once - Cache countries and languages in ContentManagementBloc - Update CreateSourceBloc to use cached lists - Simplify UI logic for dropdowns --- .../create_source/create_source_bloc.dart | 156 +----------------- .../create_source/create_source_event.dart | 5 - .../create_source/create_source_state.dart | 32 ---- .../view/create_source_page.dart | 41 +++-- 4 files changed, 28 insertions(+), 206 deletions(-) diff --git a/lib/content_management/bloc/create_source/create_source_bloc.dart b/lib/content_management/bloc/create_source/create_source_bloc.dart index 7a6beee6..2f870ff8 100644 --- a/lib/content_management/bloc/create_source/create_source_bloc.dart +++ b/lib/content_management/bloc/create_source/create_source_bloc.dart @@ -8,26 +8,20 @@ import 'package:uuid/uuid.dart'; part 'create_source_event.dart'; part 'create_source_state.dart'; -final class _FetchNextCountryPage extends CreateSourceEvent { - const _FetchNextCountryPage(); -} - -final class _FetchNextLanguagePage extends CreateSourceEvent { - const _FetchNextLanguagePage(); -} - /// A BLoC to manage the state of creating a new source. class CreateSourceBloc extends Bloc { /// {@macro create_source_bloc} CreateSourceBloc({ required DataRepository sourcesRepository, - required DataRepository countriesRepository, - required DataRepository languagesRepository, - }) : _sourcesRepository = sourcesRepository, - _countriesRepository = countriesRepository, - _languagesRepository = languagesRepository, - super(const CreateSourceState()) { - on(_onDataLoaded); + required List countries, + required List languages, + }) : _sourcesRepository = sourcesRepository, + super( + CreateSourceState( + countries: countries, + languages: languages, + ), + ) { on(_onNameChanged); on(_onDescriptionChanged); on(_onUrlChanged); @@ -36,65 +30,11 @@ class CreateSourceBloc extends Bloc { on(_onHeadquartersChanged); on(_onStatusChanged); on(_onSubmitted); - on<_FetchNextCountryPage>(_onFetchNextCountryPage); - on<_FetchNextLanguagePage>(_onFetchNextLanguagePage); } final DataRepository _sourcesRepository; - final DataRepository _countriesRepository; - final DataRepository _languagesRepository; final _uuid = const Uuid(); - Future _onDataLoaded( - CreateSourceDataLoaded event, - Emitter emit, - ) async { - emit(state.copyWith(status: CreateSourceStatus.loading)); - try { - final responses = await Future.wait([ - _countriesRepository.readAll( - sort: [const SortOption('name', SortOrder.asc)], - filter: {'status': ContentStatus.active.name}, - ), - _languagesRepository.readAll( - sort: [const SortOption('name', SortOrder.asc)], - filter: {'status': ContentStatus.active.name}, - ), - ]); - final countriesPaginated = responses[0] as PaginatedResponse; - final languagesPaginated = responses[1] as PaginatedResponse; - emit( - state.copyWith( - status: CreateSourceStatus.initial, - countries: countriesPaginated.items, - countriesCursor: countriesPaginated.cursor, - countriesHasMore: countriesPaginated.hasMore, - languages: languagesPaginated.items, - languagesCursor: languagesPaginated.cursor, - languagesHasMore: languagesPaginated.hasMore, - ), - ); - - // After the initial page is loaded, start background processes to - // fetch all remaining pages for countries and languages. - if (state.countriesHasMore) { - add(const _FetchNextCountryPage()); - } - if (state.languagesHasMore) { - add(const _FetchNextLanguagePage()); - } - } on HttpException catch (e) { - emit(state.copyWith(status: CreateSourceStatus.failure, exception: e)); - } catch (e) { - emit( - state.copyWith( - status: CreateSourceStatus.failure, - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); - } - } - void _onNameChanged( CreateSourceNameChanged event, Emitter emit, @@ -150,84 +90,6 @@ class CreateSourceBloc extends Bloc { } // --- Background Data Fetching for Dropdown --- - // The DropdownButtonFormField widget does not natively support on-scroll - // pagination. To preserve UI consistency across the application, this BLoC - // employs an event-driven background fetching mechanism. - // - // After the first page of items is loaded, a chain of events is initiated - // to progressively fetch all remaining pages. This process is throttled - // and runs in the background, ensuring the UI remains responsive while the - // full list of dropdown options is populated over time. - Future _onFetchNextCountryPage( - _FetchNextCountryPage event, - Emitter emit, - ) async { - if (!state.countriesHasMore || state.countriesIsLoadingMore) return; - - try { - emit(state.copyWith(countriesIsLoadingMore: true)); - - // ignore: inference_failure_on_instance_creation - await Future.delayed(const Duration(milliseconds: 400)); - - final nextCountries = await _countriesRepository.readAll( - pagination: PaginationOptions(cursor: state.countriesCursor), - sort: [const SortOption('name', SortOrder.asc)], - ); - - emit( - state.copyWith( - countries: List.of(state.countries)..addAll(nextCountries.items), - countriesCursor: nextCountries.cursor, - countriesHasMore: nextCountries.hasMore, - countriesIsLoadingMore: false, - ), - ); - - if (nextCountries.hasMore) { - add(const _FetchNextCountryPage()); - } - } catch (e) { - emit(state.copyWith(countriesIsLoadingMore: false)); - // Optionally log the error without disrupting the user - } - } - - Future _onFetchNextLanguagePage( - _FetchNextLanguagePage event, - Emitter emit, - ) async { - if (!state.languagesHasMore || state.languagesIsLoadingMore) return; - - try { - emit(state.copyWith(languagesIsLoadingMore: true)); - - // ignore: inference_failure_on_instance_creation - await Future.delayed(const Duration(milliseconds: 400)); - - final nextLanguages = await _languagesRepository.readAll( - pagination: PaginationOptions(cursor: state.languagesCursor), - sort: [const SortOption('name', SortOrder.asc)], - ); - - emit( - state.copyWith( - languages: List.of(state.languages)..addAll(nextLanguages.items), - languagesCursor: nextLanguages.cursor, - languagesHasMore: nextLanguages.hasMore, - languagesIsLoadingMore: false, - ), - ); - - if (nextLanguages.hasMore) { - add(const _FetchNextLanguagePage()); - } - } catch (e) { - emit(state.copyWith(languagesIsLoadingMore: false)); - // Optionally log the error without disrupting the user - } - } - Future _onSubmitted( CreateSourceSubmitted event, Emitter emit, diff --git a/lib/content_management/bloc/create_source/create_source_event.dart b/lib/content_management/bloc/create_source/create_source_event.dart index e4c4f566..c2351b04 100644 --- a/lib/content_management/bloc/create_source/create_source_event.dart +++ b/lib/content_management/bloc/create_source/create_source_event.dart @@ -8,11 +8,6 @@ sealed class CreateSourceEvent extends Equatable { List get props => []; } -/// Event to signal that the data for dropdowns should be loaded. -final class CreateSourceDataLoaded extends CreateSourceEvent { - const CreateSourceDataLoaded(); -} - /// Event for when the source's name is changed. final class CreateSourceNameChanged extends CreateSourceEvent { const CreateSourceNameChanged(this.name); diff --git a/lib/content_management/bloc/create_source/create_source_state.dart b/lib/content_management/bloc/create_source/create_source_state.dart index 8d358da7..d8f7ac67 100644 --- a/lib/content_management/bloc/create_source/create_source_state.dart +++ b/lib/content_management/bloc/create_source/create_source_state.dart @@ -30,13 +30,7 @@ final class CreateSourceState extends Equatable { this.language, this.headquarters, this.countries = const [], - this.countriesHasMore = true, - this.countriesIsLoadingMore = false, - this.countriesCursor, this.languages = const [], - this.languagesHasMore = true, - this.languagesIsLoadingMore = false, - this.languagesCursor, this.contentStatus = ContentStatus.active, this.exception, this.createdSource, @@ -50,13 +44,7 @@ final class CreateSourceState extends Equatable { final Language? language; final Country? headquarters; final List countries; - final bool countriesHasMore; - final bool countriesIsLoadingMore; - final String? countriesCursor; final List languages; - final bool languagesHasMore; - final bool languagesIsLoadingMore; - final String? languagesCursor; final ContentStatus contentStatus; final HttpException? exception; final Source? createdSource; @@ -79,13 +67,7 @@ final class CreateSourceState extends Equatable { ValueGetter? language, ValueGetter? headquarters, List? countries, - bool? countriesHasMore, - bool? countriesIsLoadingMore, - String? countriesCursor, List? languages, - bool? languagesHasMore, - bool? languagesIsLoadingMore, - String? languagesCursor, ContentStatus? contentStatus, HttpException? exception, Source? createdSource, @@ -99,15 +81,7 @@ final class CreateSourceState extends Equatable { language: language != null ? language() : this.language, headquarters: headquarters != null ? headquarters() : this.headquarters, countries: countries ?? this.countries, - countriesHasMore: countriesHasMore ?? this.countriesHasMore, - countriesIsLoadingMore: - countriesIsLoadingMore ?? this.countriesIsLoadingMore, - countriesCursor: countriesCursor ?? this.countriesCursor, languages: languages ?? this.languages, - languagesHasMore: languagesHasMore ?? this.languagesHasMore, - languagesIsLoadingMore: - languagesIsLoadingMore ?? this.languagesIsLoadingMore, - languagesCursor: languagesCursor ?? this.languagesCursor, contentStatus: contentStatus ?? this.contentStatus, exception: exception, createdSource: createdSource ?? this.createdSource, @@ -124,13 +98,7 @@ final class CreateSourceState extends Equatable { language, headquarters, countries, - countriesHasMore, - countriesIsLoadingMore, - countriesCursor, languages, - languagesHasMore, - languagesIsLoadingMore, - languagesCursor, contentStatus, exception, createdSource, diff --git a/lib/content_management/view/create_source_page.dart b/lib/content_management/view/create_source_page.dart index 2f3ef8c6..cc6d6477 100644 --- a/lib/content_management/view/create_source_page.dart +++ b/lib/content_management/view/create_source_page.dart @@ -21,12 +21,19 @@ class CreateSourcePage extends StatelessWidget { @override Widget build(BuildContext context) { + // The lists of all countries and languages are fetched once and cached in + // the ContentManagementBloc. We read them here and provide them to the + // CreateSourceBloc. + final contentManagementState = context.read().state; + final allCountries = contentManagementState.allCountries; + final allLanguages = contentManagementState.allLanguages; + return BlocProvider( create: (context) => CreateSourceBloc( sourcesRepository: context.read>(), - countriesRepository: context.read>(), - languagesRepository: context.read>(), - )..add(const CreateSourceDataLoaded()), + countries: allCountries, + languages: allLanguages, + ), child: const _CreateSourceView(), ); } @@ -114,9 +121,9 @@ class _CreateSourceViewState extends State<_CreateSourceView> { if (state.status == CreateSourceStatus.failure) { return FailureStateWidget( exception: state.exception!, - onRetry: () => context.read().add( - const CreateSourceDataLoaded(), - ), + onRetry: () => context + .read() + .add(const SharedDataRequested()), ); } @@ -167,9 +174,6 @@ class _CreateSourceViewState extends State<_CreateSourceView> { decoration: InputDecoration( labelText: l10n.language, border: const OutlineInputBorder(), - helperText: state.languagesIsLoadingMore - ? l10n.loadingFullList - : null, ), items: [ DropdownMenuItem(value: null, child: Text(l10n.none)), @@ -180,11 +184,9 @@ class _CreateSourceViewState extends State<_CreateSourceView> { ), ), ], - onChanged: state.languagesIsLoadingMore - ? null - : (value) => context.read().add( - CreateSourceLanguageChanged(value), - ), + onChanged: (value) => context + .read() + .add(CreateSourceLanguageChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( @@ -212,9 +214,6 @@ class _CreateSourceViewState extends State<_CreateSourceView> { decoration: InputDecoration( labelText: l10n.headquarters, border: const OutlineInputBorder(), - helperText: state.countriesIsLoadingMore - ? l10n.loadingFullList - : null, ), items: [ DropdownMenuItem(value: null, child: Text(l10n.none)), @@ -241,11 +240,9 @@ class _CreateSourceViewState extends State<_CreateSourceView> { ), ), ], - onChanged: state.countriesIsLoadingMore - ? null - : (value) => context.read().add( - CreateSourceHeadquartersChanged(value), - ), + onChanged: (value) => context + .read() + .add(CreateSourceHeadquartersChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( From 3a7199ed146ae56a68a7038580b2ed83c806ae0c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 19:10:48 +0100 Subject: [PATCH 6/9] refactor(content_management): simplify source editing process - Remove pagination and background fetching for countries and languages - Load full lists of countries and languages at once - Cache countries and languages in ContentManagementBloc - Update EditSourceBloc to use cached lists - Simplify UI by removing loading indicators for dropdowns --- .../bloc/edit_source/edit_source_bloc.dart | 151 ++---------------- .../bloc/edit_source/edit_source_state.dart | 32 ---- .../view/edit_source_page.dart | 33 ++-- 3 files changed, 27 insertions(+), 189 deletions(-) diff --git a/lib/content_management/bloc/edit_source/edit_source_bloc.dart b/lib/content_management/bloc/edit_source/edit_source_bloc.dart index c5644573..09dcaed5 100644 --- a/lib/content_management/bloc/edit_source/edit_source_bloc.dart +++ b/lib/content_management/bloc/edit_source/edit_source_bloc.dart @@ -7,27 +7,22 @@ import 'package:flutter/foundation.dart'; part 'edit_source_event.dart'; part 'edit_source_state.dart'; -final class _FetchNextCountryPage extends EditSourceEvent { - const _FetchNextCountryPage(); -} - -final class _FetchNextLanguagePage extends EditSourceEvent { - const _FetchNextLanguagePage(); -} - /// A BLoC to manage the state of editing a single source. class EditSourceBloc extends Bloc { /// {@macro edit_source_bloc} EditSourceBloc({ required DataRepository sourcesRepository, - required DataRepository countriesRepository, - required DataRepository languagesRepository, + required List countries, + required List languages, required String sourceId, - }) : _sourcesRepository = sourcesRepository, - _countriesRepository = countriesRepository, - _languagesRepository = languagesRepository, - _sourceId = sourceId, - super(const EditSourceState()) { + }) : _sourcesRepository = sourcesRepository, + _sourceId = sourceId, + super( + EditSourceState( + countries: countries, + languages: languages, + ), + ) { on(_onLoaded); on(_onNameChanged); on(_onDescriptionChanged); @@ -37,13 +32,9 @@ class EditSourceBloc extends Bloc { on(_onHeadquartersChanged); on(_onStatusChanged); on(_onSubmitted); - on<_FetchNextCountryPage>(_onFetchNextCountryPage); - on<_FetchNextLanguagePage>(_onFetchNextLanguagePage); } final DataRepository _sourcesRepository; - final DataRepository _countriesRepository; - final DataRepository _languagesRepository; final String _sourceId; Future _onLoaded( @@ -52,32 +43,7 @@ class EditSourceBloc extends Bloc { ) async { emit(state.copyWith(status: EditSourceStatus.loading)); try { - final responses = await Future.wait([ - _sourcesRepository.read(id: _sourceId), - _countriesRepository.readAll( - sort: [const SortOption('name', SortOrder.asc)], - filter: {'status': ContentStatus.active.name}, - ), - _languagesRepository.readAll( - sort: [const SortOption('name', SortOrder.asc)], - filter: {'status': ContentStatus.active.name}, - ), - ]); - - final source = responses[0] as Source; - final countriesPaginated = responses[1] as PaginatedResponse; - final languagesPaginated = responses[2] as PaginatedResponse; - - Language? selectedLanguage; - try { - // Find the equivalent language object from the full list. - // This ensures the DropdownButton can identify it by reference. - selectedLanguage = languagesPaginated.items.firstWhere( - (listLanguage) => listLanguage.id == source.language.id, - ); - } catch (_) { - selectedLanguage = source.language; - } + final source = await _sourcesRepository.read(id: _sourceId); emit( state.copyWith( @@ -87,26 +53,11 @@ class EditSourceBloc extends Bloc { description: source.description, url: source.url, sourceType: () => source.sourceType, - language: () => selectedLanguage, + language: () => source.language, headquarters: () => source.headquarters, contentStatus: source.status, - countries: countriesPaginated.items, - countriesCursor: countriesPaginated.cursor, - countriesHasMore: countriesPaginated.hasMore, - languages: languagesPaginated.items, - languagesCursor: languagesPaginated.cursor, - languagesHasMore: languagesPaginated.hasMore, ), ); - - // After the initial page is loaded, start background processes to - // fetch all remaining pages for countries and languages. - if (state.countriesHasMore) { - add(const _FetchNextCountryPage()); - } - if (state.languagesHasMore) { - add(const _FetchNextLanguagePage()); - } } on HttpException catch (e) { emit(state.copyWith(status: EditSourceStatus.failure, exception: e)); } catch (e) { @@ -194,84 +145,6 @@ class EditSourceBloc extends Bloc { } // --- Background Data Fetching for Dropdown --- - // The DropdownButtonFormField widget does not natively support on-scroll - // pagination. To preserve UI consistency across the application, this BLoC - // employs an event-driven background fetching mechanism. - // - // After the first page of items is loaded, a chain of events is initiated - // to progressively fetch all remaining pages. This process is throttled - // and runs in the background, ensuring the UI remains responsive while the - // full list of dropdown options is populated over time. - Future _onFetchNextCountryPage( - _FetchNextCountryPage event, - Emitter emit, - ) async { - if (!state.countriesHasMore || state.countriesIsLoadingMore) return; - - try { - emit(state.copyWith(countriesIsLoadingMore: true)); - - // ignore: inference_failure_on_instance_creation - await Future.delayed(const Duration(milliseconds: 400)); - - final nextCountries = await _countriesRepository.readAll( - pagination: PaginationOptions(cursor: state.countriesCursor), - sort: [const SortOption('name', SortOrder.asc)], - ); - - emit( - state.copyWith( - countries: List.of(state.countries)..addAll(nextCountries.items), - countriesCursor: nextCountries.cursor, - countriesHasMore: nextCountries.hasMore, - countriesIsLoadingMore: false, - ), - ); - - if (nextCountries.hasMore) { - add(const _FetchNextCountryPage()); - } - } catch (e) { - emit(state.copyWith(countriesIsLoadingMore: false)); - // Optionally log the error without disrupting the user - } - } - - Future _onFetchNextLanguagePage( - _FetchNextLanguagePage event, - Emitter emit, - ) async { - if (!state.languagesHasMore || state.languagesIsLoadingMore) return; - - try { - emit(state.copyWith(languagesIsLoadingMore: true)); - - // ignore: inference_failure_on_instance_creation - await Future.delayed(const Duration(milliseconds: 400)); - - final nextLanguages = await _languagesRepository.readAll( - pagination: PaginationOptions(cursor: state.languagesCursor), - sort: [const SortOption('name', SortOrder.asc)], - ); - - emit( - state.copyWith( - languages: List.of(state.languages)..addAll(nextLanguages.items), - languagesCursor: nextLanguages.cursor, - languagesHasMore: nextLanguages.hasMore, - languagesIsLoadingMore: false, - ), - ); - - if (nextLanguages.hasMore) { - add(const _FetchNextLanguagePage()); - } - } catch (e) { - emit(state.copyWith(languagesIsLoadingMore: false)); - // Optionally log the error without disrupting the user - } - } - Future _onSubmitted( EditSourceSubmitted event, Emitter emit, diff --git a/lib/content_management/bloc/edit_source/edit_source_state.dart b/lib/content_management/bloc/edit_source/edit_source_state.dart index c40b167d..1c65c6ae 100644 --- a/lib/content_management/bloc/edit_source/edit_source_state.dart +++ b/lib/content_management/bloc/edit_source/edit_source_state.dart @@ -30,13 +30,7 @@ final class EditSourceState extends Equatable { this.language, this.headquarters, this.countries = const [], - this.countriesHasMore = true, - this.countriesIsLoadingMore = false, - this.countriesCursor, this.languages = const [], - this.languagesHasMore = true, - this.languagesIsLoadingMore = false, - this.languagesCursor, this.contentStatus = ContentStatus.active, this.exception, this.updatedSource, @@ -51,13 +45,7 @@ final class EditSourceState extends Equatable { final Language? language; final Country? headquarters; final List countries; - final bool countriesHasMore; - final bool countriesIsLoadingMore; - final String? countriesCursor; final List languages; - final bool languagesHasMore; - final bool languagesIsLoadingMore; - final String? languagesCursor; final ContentStatus contentStatus; final HttpException? exception; final Source? updatedSource; @@ -81,13 +69,7 @@ final class EditSourceState extends Equatable { ValueGetter? language, ValueGetter? headquarters, List? countries, - bool? countriesHasMore, - bool? countriesIsLoadingMore, - String? countriesCursor, List? languages, - bool? languagesHasMore, - bool? languagesIsLoadingMore, - String? languagesCursor, ContentStatus? contentStatus, HttpException? exception, Source? updatedSource, @@ -102,15 +84,7 @@ final class EditSourceState extends Equatable { language: language != null ? language() : this.language, headquarters: headquarters != null ? headquarters() : this.headquarters, countries: countries ?? this.countries, - countriesHasMore: countriesHasMore ?? this.countriesHasMore, - countriesIsLoadingMore: - countriesIsLoadingMore ?? this.countriesIsLoadingMore, - countriesCursor: countriesCursor ?? this.countriesCursor, languages: languages ?? this.languages, - languagesHasMore: languagesHasMore ?? this.languagesHasMore, - languagesIsLoadingMore: - languagesIsLoadingMore ?? this.languagesIsLoadingMore, - languagesCursor: languagesCursor ?? this.languagesCursor, contentStatus: contentStatus ?? this.contentStatus, exception: exception, updatedSource: updatedSource ?? this.updatedSource, @@ -128,13 +102,7 @@ final class EditSourceState extends Equatable { language, headquarters, countries, - countriesHasMore, - countriesIsLoadingMore, - countriesCursor, languages, - languagesHasMore, - languagesIsLoadingMore, - languagesCursor, contentStatus, exception, updatedSource, diff --git a/lib/content_management/view/edit_source_page.dart b/lib/content_management/view/edit_source_page.dart index 3ef6abaa..654e6ed4 100644 --- a/lib/content_management/view/edit_source_page.dart +++ b/lib/content_management/view/edit_source_page.dart @@ -24,11 +24,18 @@ class EditSourcePage extends StatelessWidget { @override Widget build(BuildContext context) { + // The lists of all countries and languages are fetched once and cached in + // the ContentManagementBloc. We read them here and provide them to the + // EditSourceBloc. + final contentManagementState = context.read().state; + final allCountries = contentManagementState.allCountries; + final allLanguages = contentManagementState.allLanguages; + return BlocProvider( create: (context) => EditSourceBloc( sourcesRepository: context.read>(), - countriesRepository: context.read>(), - languagesRepository: context.read>(), + countries: allCountries, + languages: allLanguages, sourceId: sourceId, )..add(const EditSourceLoaded()), child: const _EditSourceView(), @@ -197,9 +204,6 @@ class _EditSourceViewState extends State<_EditSourceView> { decoration: InputDecoration( labelText: l10n.language, border: const OutlineInputBorder(), - helperText: state.languagesIsLoadingMore - ? l10n.loadingFullList - : null, ), items: [ DropdownMenuItem(value: null, child: Text(l10n.none)), @@ -210,11 +214,9 @@ class _EditSourceViewState extends State<_EditSourceView> { ), ), ], - onChanged: state.languagesIsLoadingMore - ? null - : (value) => context.read().add( - EditSourceLanguageChanged(value), - ), + onChanged: (value) => context + .read() + .add(EditSourceLanguageChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( @@ -242,9 +244,6 @@ class _EditSourceViewState extends State<_EditSourceView> { decoration: InputDecoration( labelText: l10n.headquarters, border: const OutlineInputBorder(), - helperText: state.countriesIsLoadingMore - ? l10n.loadingFullList - : null, ), items: [ DropdownMenuItem(value: null, child: Text(l10n.none)), @@ -271,11 +270,9 @@ class _EditSourceViewState extends State<_EditSourceView> { ), ), ], - onChanged: state.countriesIsLoadingMore - ? null - : (value) => context.read().add( - EditSourceHeadquartersChanged(value), - ), + onChanged: (value) => context + .read() + .add(EditSourceHeadquartersChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( From 096b98401d042fd2ec2e1cebc2c87c38efc4e0c5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 19:14:22 +0100 Subject: [PATCH 7/9] lint: misc --- lib/content_management/view/archived_sources_page.dart | 1 - lib/content_management/view/archived_topics_page.dart | 1 - lib/content_management/view/create_headline_page.dart | 1 - lib/content_management/view/create_source_page.dart | 1 - lib/content_management/view/create_topic_page.dart | 1 - lib/content_management/view/edit_headline_page.dart | 1 - lib/content_management/view/edit_source_page.dart | 1 - lib/content_management/view/edit_topic_page.dart | 1 - 8 files changed, 8 deletions(-) diff --git a/lib/content_management/view/archived_sources_page.dart b/lib/content_management/view/archived_sources_page.dart index c8a4f573..29f14fcc 100644 --- a/lib/content_management/view/archived_sources_page.dart +++ b/lib/content_management/view/archived_sources_page.dart @@ -5,7 +5,6 @@ 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/content_management/bloc/content_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/bloc/dashboard_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'; diff --git a/lib/content_management/view/archived_topics_page.dart b/lib/content_management/view/archived_topics_page.dart index 315ffe03..9622091d 100644 --- a/lib/content_management/view/archived_topics_page.dart +++ b/lib/content_management/view/archived_topics_page.dart @@ -5,7 +5,6 @@ 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/content_management/bloc/content_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/bloc/dashboard_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'; diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 36b0b572..285ec371 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -4,7 +4,6 @@ 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/content_management/bloc/create_headline/create_headline_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/bloc/dashboard_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:go_router/go_router.dart'; diff --git a/lib/content_management/view/create_source_page.dart b/lib/content_management/view/create_source_page.dart index cc6d6477..6e45003a 100644 --- a/lib/content_management/view/create_source_page.dart +++ b/lib/content_management/view/create_source_page.dart @@ -4,7 +4,6 @@ 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/content_management/bloc/create_source/create_source_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/bloc/dashboard_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/extensions/source_type_l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; diff --git a/lib/content_management/view/create_topic_page.dart b/lib/content_management/view/create_topic_page.dart index 6fdac2a9..575ca938 100644 --- a/lib/content_management/view/create_topic_page.dart +++ b/lib/content_management/view/create_topic_page.dart @@ -4,7 +4,6 @@ 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/content_management/bloc/create_topic/create_topic_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/bloc/dashboard_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:go_router/go_router.dart'; diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index 49bcbf36..f28c7edb 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -4,7 +4,6 @@ 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/content_management/bloc/edit_headline/edit_headline_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/bloc/dashboard_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:go_router/go_router.dart'; diff --git a/lib/content_management/view/edit_source_page.dart b/lib/content_management/view/edit_source_page.dart index 654e6ed4..077d9322 100644 --- a/lib/content_management/view/edit_source_page.dart +++ b/lib/content_management/view/edit_source_page.dart @@ -4,7 +4,6 @@ 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/content_management/bloc/edit_source/edit_source_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/bloc/dashboard_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/extensions/source_type_l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; diff --git a/lib/content_management/view/edit_topic_page.dart b/lib/content_management/view/edit_topic_page.dart index 04c3008b..92f24213 100644 --- a/lib/content_management/view/edit_topic_page.dart +++ b/lib/content_management/view/edit_topic_page.dart @@ -4,7 +4,6 @@ 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/content_management/bloc/edit_topic/edit_topic_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/bloc/dashboard_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:go_router/go_router.dart'; From a9279cee51b7db46583047cda8974f96886588ed Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 19:14:50 +0100 Subject: [PATCH 8/9] style: misc --- lib/app/view/app.dart | 4 +- .../archived_headlines_bloc.dart | 4 +- .../archived_headlines_state.dart | 14 +- .../archived_sources_bloc.dart | 4 +- .../archived_sources_state.dart | 14 +- .../archived_topics/archived_topics_bloc.dart | 4 +- .../archived_topics_state.dart | 14 +- .../bloc/content_management_bloc.dart | 12 +- .../create_headline/create_headline_bloc.dart | 8 +- .../create_source/create_source_bloc.dart | 14 +- .../edit_headline/edit_headline_bloc.dart | 10 +- .../bloc/edit_source/edit_source_bloc.dart | 16 +- .../view/archived_headlines_page.dart | 148 ++++++++--------- .../view/archived_sources_page.dart | 148 ++++++++--------- .../view/archived_topics_page.dart | 150 +++++++++--------- .../view/content_management_page.dart | 6 +- .../view/create_headline_page.dart | 5 +- .../view/create_source_page.dart | 6 +- .../view/edit_headline_page.dart | 6 +- .../view/edit_source_page.dart | 12 +- .../view/headlines_page.dart | 22 +-- lib/content_management/view/sources_page.dart | 14 +- lib/content_management/view/topics_page.dart | 22 +-- 23 files changed, 332 insertions(+), 325 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index f47f208c..fe75ef40 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -114,8 +114,8 @@ class App extends StatelessWidget { ), BlocProvider( create: (context) => DashboardBloc( - dashboardSummaryRepository: - context.read>(), + dashboardSummaryRepository: context + .read>(), headlinesRepository: context.read>(), topicsRepository: context.read>(), sourcesRepository: context.read>(), 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 ac4d22f3..709b9a73 100644 --- a/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart @@ -10,8 +10,8 @@ class ArchivedHeadlinesBloc extends Bloc { ArchivedHeadlinesBloc({ required DataRepository headlinesRepository, - }) : _headlinesRepository = headlinesRepository, - super(const ArchivedHeadlinesState()) { + }) : _headlinesRepository = headlinesRepository, + super(const ArchivedHeadlinesState()) { on(_onLoadArchivedHeadlinesRequested); on(_onRestoreHeadlineRequested); on(_onDeleteHeadlineForeverRequested); 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 57fcec10..22f4bbd0 100644 --- a/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart @@ -46,11 +46,11 @@ class ArchivedHeadlinesState extends Equatable { @override List get props => [ - status, - headlines, - cursor, - hasMore, - exception, - restoredHeadline, - ]; + status, + headlines, + cursor, + hasMore, + exception, + restoredHeadline, + ]; } 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 45fa8efb..97758a9c 100644 --- a/lib/content_management/bloc/archived_sources/archived_sources_bloc.dart +++ b/lib/content_management/bloc/archived_sources/archived_sources_bloc.dart @@ -10,8 +10,8 @@ class ArchivedSourcesBloc extends Bloc { ArchivedSourcesBloc({ required DataRepository sourcesRepository, - }) : _sourcesRepository = sourcesRepository, - super(const ArchivedSourcesState()) { + }) : _sourcesRepository = sourcesRepository, + super(const ArchivedSourcesState()) { on(_onLoadArchivedSourcesRequested); on(_onRestoreSourceRequested); } 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 4685a4d2..67e43b1e 100644 --- a/lib/content_management/bloc/archived_sources/archived_sources_state.dart +++ b/lib/content_management/bloc/archived_sources/archived_sources_state.dart @@ -46,11 +46,11 @@ class ArchivedSourcesState extends Equatable { @override List get props => [ - status, - sources, - cursor, - hasMore, - exception, - restoredSource, - ]; + status, + sources, + cursor, + hasMore, + exception, + restoredSource, + ]; } 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 7e3a66c4..a991fad8 100644 --- a/lib/content_management/bloc/archived_topics/archived_topics_bloc.dart +++ b/lib/content_management/bloc/archived_topics/archived_topics_bloc.dart @@ -10,8 +10,8 @@ class ArchivedTopicsBloc extends Bloc { ArchivedTopicsBloc({ required DataRepository topicsRepository, - }) : _topicsRepository = topicsRepository, - super(const ArchivedTopicsState()) { + }) : _topicsRepository = topicsRepository, + super(const ArchivedTopicsState()) { on(_onLoadArchivedTopicsRequested); on(_onRestoreTopicRequested); } 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 e13a7850..2afa306a 100644 --- a/lib/content_management/bloc/archived_topics/archived_topics_state.dart +++ b/lib/content_management/bloc/archived_topics/archived_topics_state.dart @@ -46,11 +46,11 @@ class ArchivedTopicsState extends Equatable { @override List get props => [ - status, - topics, - cursor, - hasMore, - exception, - restoredTopic, - ]; + status, + topics, + cursor, + hasMore, + exception, + restoredTopic, + ]; } diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index e5958afd..6b50e560 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -26,12 +26,12 @@ class ContentManagementBloc required DataRepository sourcesRepository, required DataRepository countriesRepository, required DataRepository languagesRepository, - }) : _headlinesRepository = headlinesRepository, - _topicsRepository = topicsRepository, - _sourcesRepository = sourcesRepository, - _countriesRepository = countriesRepository, - _languagesRepository = languagesRepository, - super(const ContentManagementState()) { + }) : _headlinesRepository = headlinesRepository, + _topicsRepository = topicsRepository, + _sourcesRepository = sourcesRepository, + _countriesRepository = countriesRepository, + _languagesRepository = languagesRepository, + super(const ContentManagementState()) { on(_onSharedDataRequested); on(_onContentManagementTabChanged); on(_onLoadHeadlinesRequested); diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index aa409243..91520e60 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -17,10 +17,10 @@ class CreateHeadlineBloc required DataRepository sourcesRepository, required DataRepository topicsRepository, required List countries, - }) : _headlinesRepository = headlinesRepository, - _sourcesRepository = sourcesRepository, - _topicsRepository = topicsRepository, - super(CreateHeadlineState(countries: countries)) { + }) : _headlinesRepository = headlinesRepository, + _sourcesRepository = sourcesRepository, + _topicsRepository = topicsRepository, + super(CreateHeadlineState(countries: countries)) { on(_onDataLoaded); on(_onTitleChanged); on(_onExcerptChanged); diff --git a/lib/content_management/bloc/create_source/create_source_bloc.dart b/lib/content_management/bloc/create_source/create_source_bloc.dart index 2f870ff8..0f99f7fa 100644 --- a/lib/content_management/bloc/create_source/create_source_bloc.dart +++ b/lib/content_management/bloc/create_source/create_source_bloc.dart @@ -15,13 +15,13 @@ class CreateSourceBloc extends Bloc { required DataRepository sourcesRepository, required List countries, required List languages, - }) : _sourcesRepository = sourcesRepository, - super( - CreateSourceState( - countries: countries, - languages: languages, - ), - ) { + }) : _sourcesRepository = sourcesRepository, + super( + CreateSourceState( + countries: countries, + languages: languages, + ), + ) { on(_onNameChanged); on(_onDescriptionChanged); on(_onUrlChanged); diff --git a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart index 10f398f8..6fc464f3 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart @@ -16,11 +16,11 @@ class EditHeadlineBloc extends Bloc { required DataRepository topicsRepository, required List countries, required String headlineId, - }) : _headlinesRepository = headlinesRepository, - _sourcesRepository = sourcesRepository, - _topicsRepository = topicsRepository, - _headlineId = headlineId, - super(EditHeadlineState(countries: countries)) { + }) : _headlinesRepository = headlinesRepository, + _sourcesRepository = sourcesRepository, + _topicsRepository = topicsRepository, + _headlineId = headlineId, + super(EditHeadlineState(countries: countries)) { on(_onLoaded); on(_onTitleChanged); on(_onExcerptChanged); diff --git a/lib/content_management/bloc/edit_source/edit_source_bloc.dart b/lib/content_management/bloc/edit_source/edit_source_bloc.dart index 09dcaed5..3c5f78b1 100644 --- a/lib/content_management/bloc/edit_source/edit_source_bloc.dart +++ b/lib/content_management/bloc/edit_source/edit_source_bloc.dart @@ -15,14 +15,14 @@ class EditSourceBloc extends Bloc { required List countries, required List languages, required String sourceId, - }) : _sourcesRepository = sourcesRepository, - _sourceId = sourceId, - super( - EditSourceState( - countries: countries, - languages: languages, - ), - ) { + }) : _sourcesRepository = sourcesRepository, + _sourceId = sourceId, + super( + EditSourceState( + countries: countries, + languages: languages, + ), + ) { on(_onLoaded); on(_onNameChanged); on(_onDescriptionChanged); diff --git a/lib/content_management/view/archived_headlines_page.dart b/lib/content_management/view/archived_headlines_page.dart index 0898c4de..13e3dbd8 100644 --- a/lib/content_management/view/archived_headlines_page.dart +++ b/lib/content_management/view/archived_headlines_page.dart @@ -44,8 +44,8 @@ class _ArchivedHeadlinesView extends StatelessWidget { if (state.status == ArchivedHeadlinesStatus.success && state.restoredHeadline != null) { context.read().add( - const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), - ); + const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), + ); } }, child: BlocBuilder( @@ -59,80 +59,80 @@ class _ArchivedHeadlinesView extends StatelessWidget { ); } - if (state.status == ArchivedHeadlinesStatus.failure) { - return FailureStateWidget( - exception: state.exception!, - onRetry: () => context.read().add( - const LoadArchivedHeadlinesRequested( - limit: kDefaultRowsPerPage, - ), + 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)); - } + 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, + 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, ), - 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, ), - 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, ), - ), - ], - ); + ], + ); }, ), ), @@ -183,8 +183,8 @@ class _HeadlinesDataSource extends DataTableSource { tooltip: l10n.restore, onPressed: () { context.read().add( - RestoreHeadlineRequested(headline.id), - ); + RestoreHeadlineRequested(headline.id), + ); }, ), IconButton( @@ -192,8 +192,8 @@ class _HeadlinesDataSource extends DataTableSource { tooltip: l10n.deleteForever, onPressed: () { context.read().add( - DeleteHeadlineForeverRequested(headline.id), - ); + DeleteHeadlineForeverRequested(headline.id), + ); }, ), ], diff --git a/lib/content_management/view/archived_sources_page.dart b/lib/content_management/view/archived_sources_page.dart index 29f14fcc..b9a5fdb8 100644 --- a/lib/content_management/view/archived_sources_page.dart +++ b/lib/content_management/view/archived_sources_page.dart @@ -42,91 +42,91 @@ class _ArchivedSourcesView extends StatelessWidget { listener: (context, state) { if (state.restoredSource != null) { context.read().add( - const LoadSourcesRequested(limit: kDefaultRowsPerPage), - ); + const LoadSourcesRequested(limit: kDefaultRowsPerPage), + ); } }, child: BlocBuilder( builder: (context, state) { if (state.status == ArchivedSourcesStatus.loading && state.sources.isEmpty) { - return LoadingStateWidget( - icon: Icons.source, - headline: l10n.loadingArchivedSources, - subheadline: l10n.pleaseWait, - ); - } + return LoadingStateWidget( + icon: Icons.source, + headline: l10n.loadingArchivedSources, + subheadline: l10n.pleaseWait, + ); + } - if (state.status == ArchivedSourcesStatus.failure) { - return FailureStateWidget( - exception: state.exception!, - onRetry: () => context.read().add( - const LoadArchivedSourcesRequested( - limit: kDefaultRowsPerPage, - ), + 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(l10n.noArchivedSourcesFound)); - } + if (state.sources.isEmpty) { + return Center(child: Text(l10n.noArchivedSourcesFound)); + } - 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, + 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, ), - 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, ), - 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, ), - ), - ], - ); + ], + ); }, ), ), @@ -176,8 +176,8 @@ class _SourcesDataSource extends DataTableSource { tooltip: l10n.restore, onPressed: () { context.read().add( - RestoreSourceRequested(source.id), - ); + RestoreSourceRequested(source.id), + ); }, ), ], diff --git a/lib/content_management/view/archived_topics_page.dart b/lib/content_management/view/archived_topics_page.dart index 9622091d..46db2de3 100644 --- a/lib/content_management/view/archived_topics_page.dart +++ b/lib/content_management/view/archived_topics_page.dart @@ -41,92 +41,92 @@ class _ArchivedTopicsView extends StatelessWidget { previous.restoredTopic != current.restoredTopic, listener: (context, state) { if (state.restoredTopic != null) { - context - .read() - .add(const LoadTopicsRequested(limit: kDefaultRowsPerPage)); + context.read().add( + const LoadTopicsRequested(limit: kDefaultRowsPerPage), + ); } }, child: BlocBuilder( builder: (context, state) { if (state.status == ArchivedTopicsStatus.loading && state.topics.isEmpty) { - return LoadingStateWidget( - icon: Icons.topic, - headline: l10n.loadingArchivedTopics, - subheadline: l10n.pleaseWait, - ); - } + return LoadingStateWidget( + icon: Icons.topic, + headline: l10n.loadingArchivedTopics, + subheadline: l10n.pleaseWait, + ); + } - if (state.status == ArchivedTopicsStatus.failure) { - return FailureStateWidget( - exception: state.exception!, - onRetry: () => context.read().add( - const LoadArchivedTopicsRequested( - limit: kDefaultRowsPerPage, - ), + 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)); - } + if (state.topics.isEmpty) { + return Center(child: Text(l10n.noArchivedTopicsFound)); + } - 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, + 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, ), - 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, ), - 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, ), - ), - ], - ); + ], + ); }, ), ), @@ -176,8 +176,8 @@ class _TopicsDataSource extends DataTableSource { tooltip: l10n.restore, onPressed: () { context.read().add( - RestoreTopicRequested(topic.id), - ); + RestoreTopicRequested(topic.id), + ); }, ), ], diff --git a/lib/content_management/view/content_management_page.dart b/lib/content_management/view/content_management_page.dart index 1ded64f5..5925efc3 100644 --- a/lib/content_management/view/content_management_page.dart +++ b/lib/content_management/view/content_management_page.dart @@ -96,8 +96,10 @@ class _ContentManagementPageState extends State icon: const Icon(Icons.inventory_2_outlined), tooltip: l10n.archivedItems, onPressed: () { - final currentTab = - context.read().state.activeTab; + final currentTab = context + .read() + .state + .activeTab; switch (currentTab) { case ContentManagementTab.headlines: context.goNamed(Routes.archivedHeadlinesName); diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 285ec371..3d56e203 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -22,7 +22,10 @@ class CreateHeadlinePage extends StatelessWidget { // The list of all countries is fetched once and cached in the // ContentManagementBloc. We read it here and provide it to the // CreateHeadlineBloc. - final allCountries = context.read().state.allCountries; + final allCountries = context + .read() + .state + .allCountries; return BlocProvider( create: (context) => CreateHeadlineBloc( headlinesRepository: context.read>(), diff --git a/lib/content_management/view/create_source_page.dart b/lib/content_management/view/create_source_page.dart index 6e45003a..2d44daad 100644 --- a/lib/content_management/view/create_source_page.dart +++ b/lib/content_management/view/create_source_page.dart @@ -120,9 +120,9 @@ class _CreateSourceViewState extends State<_CreateSourceView> { if (state.status == CreateSourceStatus.failure) { return FailureStateWidget( exception: state.exception!, - onRetry: () => context - .read() - .add(const SharedDataRequested()), + onRetry: () => context.read().add( + const SharedDataRequested(), + ), ); } diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index f28c7edb..f3014920 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -25,8 +25,10 @@ class EditHeadlinePage extends StatelessWidget { // The list of all countries is fetched once and cached in the // ContentManagementBloc. We read it here and provide it to the // EditHeadlineBloc. - final allCountries = - context.read().state.allCountries; + final allCountries = context + .read() + .state + .allCountries; return BlocProvider( create: (context) => EditHeadlineBloc( headlinesRepository: context.read>(), diff --git a/lib/content_management/view/edit_source_page.dart b/lib/content_management/view/edit_source_page.dart index 077d9322..185d0c4f 100644 --- a/lib/content_management/view/edit_source_page.dart +++ b/lib/content_management/view/edit_source_page.dart @@ -213,9 +213,9 @@ class _EditSourceViewState extends State<_EditSourceView> { ), ), ], - onChanged: (value) => context - .read() - .add(EditSourceLanguageChanged(value)), + onChanged: (value) => context.read().add( + EditSourceLanguageChanged(value), + ), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( @@ -269,9 +269,9 @@ class _EditSourceViewState extends State<_EditSourceView> { ), ), ], - onChanged: (value) => context - .read() - .add(EditSourceHeadquartersChanged(value)), + onChanged: (value) => context.read().add( + EditSourceHeadquartersChanged(value), + ), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index fd847507..c67cc695 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), + ), ); } @@ -102,11 +102,11 @@ class _HeadlinesPageState extends State { state.headlinesStatus != ContentManagementStatus.loading) { context.read().add( - LoadHeadlinesRequested( - startAfterId: state.headlinesCursor, - limit: kDefaultRowsPerPage, - ), - ); + LoadHeadlinesRequested( + startAfterId: state.headlinesCursor, + limit: kDefaultRowsPerPage, + ), + ); } }, empty: Center(child: Text(l10n.noHeadlinesFound)), @@ -189,8 +189,8 @@ class _HeadlinesDataSource extends DataTableSource { tooltip: l10n.archive, onPressed: () { context.read().add( - ArchiveHeadlineRequested(headline.id), - ); + ArchiveHeadlineRequested(headline.id), + ); }, ), ], diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index 9c14cc87..0a99314b 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -103,11 +103,11 @@ class _SourcesPageState extends State { state.sourcesStatus != ContentManagementStatus.loading) { context.read().add( - LoadSourcesRequested( - startAfterId: state.sourcesCursor, - limit: kDefaultRowsPerPage, - ), - ); + LoadSourcesRequested( + startAfterId: state.sourcesCursor, + limit: kDefaultRowsPerPage, + ), + ); } }, empty: Center(child: Text(l10n.noSourcesFound)), @@ -191,8 +191,8 @@ class _SourcesDataSource extends DataTableSource { onPressed: () { // Dispatch delete event context.read().add( - ArchiveSourceRequested(source.id), - ); + ArchiveSourceRequested(source.id), + ); }, ), ], diff --git a/lib/content_management/view/topics_page.dart b/lib/content_management/view/topics_page.dart index c9b64393..5af0fe17 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), + ), ); } @@ -97,11 +97,11 @@ class _TopicPageState extends State { state.topicsHasMore && state.topicsStatus != ContentManagementStatus.loading) { context.read().add( - LoadTopicsRequested( - startAfterId: state.topicsCursor, - limit: kDefaultRowsPerPage, - ), - ); + LoadTopicsRequested( + startAfterId: state.topicsCursor, + limit: kDefaultRowsPerPage, + ), + ); } }, empty: Center(child: Text(l10n.noTopicsFound)), @@ -184,8 +184,8 @@ class _TopicsDataSource extends DataTableSource { onPressed: () { // Dispatch delete event context.read().add( - ArchiveTopicRequested(topic.id), - ); + ArchiveTopicRequested(topic.id), + ); }, ), ], From 887a7503c64855f9d6556a61ee488a72a684e5a7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 2 Aug 2025 19:22:14 +0100 Subject: [PATCH 9/9] refactor(content_management): add documentation for background data fetching - Add comments explaining the background data fetching mechanism for countries and languages - Clarify the purpose of this mechanism in relation to UI consistency and DropdownButtonFormField limitations --- lib/content_management/bloc/content_management_bloc.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index 6b50e560..2404f166 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -51,6 +51,11 @@ class ContentManagementBloc final DataRepository _countriesRepository; final DataRepository _languagesRepository; + // --- Background Data Fetching for countries/languages for the ui Dropdown --- + // + // The DropdownButtonFormField widget does not natively support on-scroll + // pagination. To preserve UI consistency across the application, this BLoC + // employs an event-driven background fetching mechanism. Future _onSharedDataRequested( SharedDataRequested event, Emitter emit,