diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 8fa936a..85b7458 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -32,19 +32,23 @@ class App extends StatelessWidget { userContentPreferencesRepository, required DataRepository remoteConfigRepository, required DataRepository dashboardSummaryRepository, + required DataRepository countriesRepository, + required DataRepository languagesRepository, required KVStorageService storageService, required AppEnvironment environment, super.key, - }) : _authenticationRepository = authenticationRepository, - _headlinesRepository = headlinesRepository, - _topicsRepository = topicsRepository, - _sourcesRepository = sourcesRepository, - _userAppSettingsRepository = userAppSettingsRepository, - _userContentPreferencesRepository = userContentPreferencesRepository, - _remoteConfigRepository = remoteConfigRepository, - _kvStorageService = storageService, - _dashboardSummaryRepository = dashboardSummaryRepository, - _environment = environment; + }) : _authenticationRepository = authenticationRepository, + _headlinesRepository = headlinesRepository, + _topicsRepository = topicsRepository, + _sourcesRepository = sourcesRepository, + _userAppSettingsRepository = userAppSettingsRepository, + _userContentPreferencesRepository = userContentPreferencesRepository, + _remoteConfigRepository = remoteConfigRepository, + _kvStorageService = storageService, + _dashboardSummaryRepository = dashboardSummaryRepository, + _countriesRepository = countriesRepository, + _languagesRepository = languagesRepository, + _environment = environment; final AuthRepository _authenticationRepository; final DataRepository _headlinesRepository; @@ -52,9 +56,11 @@ class App extends StatelessWidget { final DataRepository _sourcesRepository; final DataRepository _userAppSettingsRepository; final DataRepository - _userContentPreferencesRepository; + _userContentPreferencesRepository; final DataRepository _remoteConfigRepository; final DataRepository _dashboardSummaryRepository; + final DataRepository _countriesRepository; + final DataRepository _languagesRepository; final KVStorageService _kvStorageService; final AppEnvironment _environment; @@ -70,6 +76,8 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _userContentPreferencesRepository), RepositoryProvider.value(value: _remoteConfigRepository), RepositoryProvider.value(value: _dashboardSummaryRepository), + RepositoryProvider.value(value: _countriesRepository), + RepositoryProvider.value(value: _languagesRepository), RepositoryProvider.value(value: _kvStorageService), ], child: MultiBlocProvider( diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 8296499..2acb46d 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -60,6 +60,8 @@ Future bootstrap( DataClient userAppSettingsClient; DataClient remoteConfigClient; DataClient dashboardSummaryClient; + DataClient countriesClient; + DataClient languagesClient; if (appConfig.environment == app_config.AppEnvironment.demo) { headlinesClient = DataInMemory( @@ -102,6 +104,18 @@ Future bootstrap( initialData: dashboardSummaryFixturesData, logger: Logger('DataInMemory'), ); + countriesClient = DataInMemory( + toJson: (i) => i.toJson(), + getId: (i) => i.id, + initialData: countriesFixturesData, + logger: Logger('DataInMemory'), + ); + languagesClient = DataInMemory( + toJson: (i) => i.toJson(), + getId: (i) => i.id, + initialData: languagesFixturesData, + logger: Logger('DataInMemory'), + ); } else if (appConfig.environment == app_config.AppEnvironment.development) { headlinesClient = DataApi( httpClient: httpClient!, @@ -152,6 +166,20 @@ Future bootstrap( toJson: (summary) => summary.toJson(), logger: Logger('DataApi'), ); + countriesClient = DataApi( + httpClient: httpClient, + modelName: 'country', + fromJson: Country.fromJson, + toJson: (country) => country.toJson(), + logger: Logger('DataApi'), + ); + languagesClient = DataApi( + httpClient: httpClient, + modelName: 'language', + fromJson: Language.fromJson, + toJson: (language) => language.toJson(), + logger: Logger('DataApi'), + ); } else { headlinesClient = DataApi( httpClient: httpClient!, @@ -202,6 +230,20 @@ Future bootstrap( toJson: (summary) => summary.toJson(), logger: Logger('DataApi'), ); + countriesClient = DataApi( + httpClient: httpClient, + modelName: 'country', + fromJson: Country.fromJson, + toJson: (country) => country.toJson(), + logger: Logger('DataApi'), + ); + languagesClient = DataApi( + httpClient: httpClient, + modelName: 'language', + fromJson: Language.fromJson, + toJson: (language) => language.toJson(), + logger: Logger('DataApi'), + ); } final headlinesRepository = DataRepository( @@ -222,6 +264,10 @@ Future bootstrap( final dashboardSummaryRepository = DataRepository( dataClient: dashboardSummaryClient, ); + final countriesRepository = + DataRepository(dataClient: countriesClient); + final languagesRepository = + DataRepository(dataClient: languagesClient); return App( authenticationRepository: authenticationRepository, @@ -232,6 +278,8 @@ Future bootstrap( userContentPreferencesRepository: userContentPreferencesRepository, remoteConfigRepository: remoteConfigRepository, dashboardSummaryRepository: dashboardSummaryRepository, + countriesRepository: countriesRepository, + languagesRepository: languagesRepository, storageService: kvStorage, environment: environment, ); 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 da10939..7a053c5 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -1,10 +1,8 @@ import 'package:bloc/bloc.dart'; import 'package:core/core.dart'; -import 'package:country_picker/country_picker.dart' as picker; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; import 'package:uuid/uuid.dart'; part 'create_headline_event.dart'; @@ -18,10 +16,12 @@ class CreateHeadlineBloc required DataRepository headlinesRepository, required DataRepository sourcesRepository, required DataRepository topicsRepository, - }) : _headlinesRepository = headlinesRepository, - _sourcesRepository = sourcesRepository, - _topicsRepository = topicsRepository, - super(const CreateHeadlineState()) { + required DataRepository countriesRepository, + }) : _headlinesRepository = headlinesRepository, + _sourcesRepository = sourcesRepository, + _topicsRepository = topicsRepository, + _countriesRepository = countriesRepository, + super(const CreateHeadlineState()) { on(_onDataLoaded); on(_onTitleChanged); on(_onExcerptChanged); @@ -37,6 +37,7 @@ class CreateHeadlineBloc final DataRepository _headlinesRepository; final DataRepository _sourcesRepository; final DataRepository _topicsRepository; + final DataRepository _countriesRepository; final _uuid = const Uuid(); Future _onDataLoaded( @@ -48,6 +49,7 @@ class CreateHeadlineBloc final [ sourcesResponse, topicsResponse, + countriesResponse, ] = await Future.wait([ _sourcesRepository.readAll( sort: [const SortOption('updatedAt', SortOrder.desc)], @@ -55,16 +57,22 @@ class CreateHeadlineBloc _topicsRepository.readAll( sort: [const SortOption('updatedAt', SortOrder.desc)], ), + _countriesRepository.readAll( + sort: [const SortOption('name', SortOrder.asc)], + ), ]); final sources = (sourcesResponse as PaginatedResponse).items; final topics = (topicsResponse as PaginatedResponse).items; + final countries = + (countriesResponse as PaginatedResponse).items; emit( state.copyWith( status: CreateHeadlineStatus.initial, sources: sources, topics: topics, + countries: countries, ), ); } on HttpException catch (e) { @@ -125,13 +133,7 @@ class CreateHeadlineBloc CreateHeadlineCountryChanged event, Emitter emit, ) { - final packageCountry = event.country; - if (packageCountry == null) { - emit(state.copyWith(eventCountry: () => null)); - } else { - final coreCountry = adaptPackageCountryToCoreCountry(packageCountry); - emit(state.copyWith(eventCountry: () => coreCountry)); - } + emit(state.copyWith(eventCountry: () => event.country)); } void _onStatusChanged( diff --git a/lib/content_management/bloc/create_headline/create_headline_event.dart b/lib/content_management/bloc/create_headline/create_headline_event.dart index a600672..655eae9 100644 --- a/lib/content_management/bloc/create_headline/create_headline_event.dart +++ b/lib/content_management/bloc/create_headline/create_headline_event.dart @@ -64,7 +64,7 @@ final class CreateHeadlineTopicChanged extends CreateHeadlineEvent { /// Event for when the headline's country is changed. final class CreateHeadlineCountryChanged extends CreateHeadlineEvent { const CreateHeadlineCountryChanged(this.country); - final picker.Country? country; + final Country? country; @override List get props => [country]; } 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 2ff304f..0739db1 100644 --- a/lib/content_management/bloc/create_headline/create_headline_state.dart +++ b/lib/content_management/bloc/create_headline/create_headline_state.dart @@ -31,6 +31,7 @@ final class CreateHeadlineState extends Equatable { this.eventCountry, this.sources = const [], this.topics = const [], + this.countries = const [], this.contentStatus = ContentStatus.active, this.exception, this.createdHeadline, @@ -46,6 +47,7 @@ final class CreateHeadlineState extends Equatable { final Country? eventCountry; final List sources; final List topics; + final List countries; final ContentStatus contentStatus; final HttpException? exception; final Headline? createdHeadline; @@ -71,6 +73,7 @@ final class CreateHeadlineState extends Equatable { ValueGetter? eventCountry, List? sources, List? topics, + List? countries, ContentStatus? contentStatus, HttpException? exception, Headline? createdHeadline, @@ -86,6 +89,7 @@ final class CreateHeadlineState extends Equatable { eventCountry: eventCountry != null ? eventCountry() : this.eventCountry, sources: sources ?? this.sources, topics: topics ?? this.topics, + countries: countries ?? this.countries, contentStatus: contentStatus ?? this.contentStatus, exception: exception, createdHeadline: createdHeadline ?? this.createdHeadline, @@ -104,6 +108,7 @@ final class CreateHeadlineState extends Equatable { eventCountry, sources, topics, + countries, contentStatus, exception, createdHeadline, 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 9325782..312f505 100644 --- a/lib/content_management/bloc/create_source/create_source_bloc.dart +++ b/lib/content_management/bloc/create_source/create_source_bloc.dart @@ -1,11 +1,8 @@ import 'package:bloc/bloc.dart'; import 'package:core/core.dart'; -import 'package:country_picker/country_picker.dart' as picker; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; -import 'package:language_picker/languages.dart'; +import 'package:flutter/foundation.dart'; import 'package:uuid/uuid.dart'; part 'create_source_event.dart'; @@ -16,8 +13,12 @@ class CreateSourceBloc extends Bloc { /// {@macro create_source_bloc} CreateSourceBloc({ required DataRepository sourcesRepository, - }) : _sourcesRepository = sourcesRepository, - super(const CreateSourceState()) { + required DataRepository countriesRepository, + required DataRepository languagesRepository, + }) : _sourcesRepository = sourcesRepository, + _countriesRepository = countriesRepository, + _languagesRepository = languagesRepository, + super(const CreateSourceState()) { on(_onDataLoaded); on(_onNameChanged); on(_onDescriptionChanged); @@ -30,15 +31,38 @@ class CreateSourceBloc extends Bloc { } final DataRepository _sourcesRepository; + final DataRepository _countriesRepository; + final DataRepository _languagesRepository; final _uuid = const Uuid(); Future _onDataLoaded( CreateSourceDataLoaded event, Emitter emit, ) async { - // This event is now a no-op since we don't need to load countries. - // We just ensure the BLoC is in the initial state. - emit(state.copyWith(status: CreateSourceStatus.initial)); + emit(state.copyWith(status: CreateSourceStatus.loading)); + try { + final [countriesResponse, languagesResponse] = await Future.wait([ + _countriesRepository.readAll( + sort: [const SortOption('name', SortOrder.asc)], + ), + _languagesRepository.readAll( + sort: [const SortOption('name', SortOrder.asc)], + ), + ]); + + final countries = (countriesResponse as PaginatedResponse).items; + final languages = (languagesResponse as PaginatedResponse).items; + + emit( + state.copyWith( + status: CreateSourceStatus.initial, + countries: countries, + languages: languages, + ), + ); + } catch (e) { + emit(state.copyWith(status: CreateSourceStatus.failure)); + } } void _onNameChanged( @@ -80,13 +104,7 @@ class CreateSourceBloc extends Bloc { CreateSourceHeadquartersChanged event, Emitter emit, ) { - final packageCountry = event.headquarters; - if (packageCountry == null) { - emit(state.copyWith(headquarters: () => null)); - } else { - final coreCountry = adaptPackageCountryToCoreCountry(packageCountry); - emit(state.copyWith(headquarters: () => coreCountry)); - } + emit(state.copyWith(headquarters: () => event.headquarters)); } void _onStatusChanged( @@ -116,7 +134,7 @@ class CreateSourceBloc extends Bloc { description: state.description, url: state.url, sourceType: state.sourceType!, - language: adaptPackageLanguageToLanguageCode(state.language!), + language: state.language!, createdAt: now, updatedAt: now, headquarters: state.headquarters!, 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 8bbf2e7..e4c4f56 100644 --- a/lib/content_management/bloc/create_source/create_source_event.dart +++ b/lib/content_management/bloc/create_source/create_source_event.dart @@ -56,7 +56,7 @@ final class CreateSourceLanguageChanged extends CreateSourceEvent { /// Event for when the source's headquarters is changed. final class CreateSourceHeadquartersChanged extends CreateSourceEvent { const CreateSourceHeadquartersChanged(this.headquarters); - final picker.Country? headquarters; + final Country? headquarters; @override List get props => [headquarters]; } 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 66165eb..d86c2c5 100644 --- a/lib/content_management/bloc/create_source/create_source_state.dart +++ b/lib/content_management/bloc/create_source/create_source_state.dart @@ -29,6 +29,8 @@ final class CreateSourceState extends Equatable { this.sourceType, this.language, this.headquarters, + this.countries = const [], + this.languages = const [], this.contentStatus = ContentStatus.active, this.exception, this.createdSource, @@ -41,6 +43,8 @@ final class CreateSourceState extends Equatable { final SourceType? sourceType; final Language? language; final Country? headquarters; + final List countries; + final List languages; final ContentStatus contentStatus; final HttpException? exception; final Source? createdSource; @@ -62,6 +66,8 @@ final class CreateSourceState extends Equatable { ValueGetter? sourceType, ValueGetter? language, ValueGetter? headquarters, + List? countries, + List? languages, ContentStatus? contentStatus, HttpException? exception, Source? createdSource, @@ -74,6 +80,8 @@ final class CreateSourceState extends Equatable { sourceType: sourceType != null ? sourceType() : this.sourceType, language: language != null ? language() : this.language, headquarters: headquarters != null ? headquarters() : this.headquarters, + countries: countries ?? this.countries, + languages: languages ?? this.languages, contentStatus: contentStatus ?? this.contentStatus, exception: exception, createdSource: createdSource ?? this.createdSource, @@ -89,6 +97,8 @@ final class CreateSourceState extends Equatable { sourceType, language, headquarters, + countries, + languages, contentStatus, exception, createdSource, 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 34fd165..02a64f4 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart @@ -1,10 +1,8 @@ import 'package:bloc/bloc.dart'; import 'package:core/core.dart'; -import 'package:country_picker/country_picker.dart' as picker; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; part 'edit_headline_event.dart'; part 'edit_headline_state.dart'; @@ -16,12 +14,14 @@ class EditHeadlineBloc extends Bloc { required DataRepository headlinesRepository, required DataRepository sourcesRepository, required DataRepository topicsRepository, + required DataRepository countriesRepository, required String headlineId, - }) : _headlinesRepository = headlinesRepository, - _sourcesRepository = sourcesRepository, - _topicsRepository = topicsRepository, - _headlineId = headlineId, - super(const EditHeadlineState()) { + }) : _headlinesRepository = headlinesRepository, + _sourcesRepository = sourcesRepository, + _topicsRepository = topicsRepository, + _countriesRepository = countriesRepository, + _headlineId = headlineId, + super(const EditHeadlineState()) { on(_onLoaded); on(_onTitleChanged); on(_onExcerptChanged); @@ -37,6 +37,7 @@ class EditHeadlineBloc extends Bloc { final DataRepository _headlinesRepository; final DataRepository _sourcesRepository; final DataRepository _topicsRepository; + final DataRepository _countriesRepository; final String _headlineId; Future _onLoaded( @@ -49,6 +50,7 @@ class EditHeadlineBloc extends Bloc { headlineResponse, sourcesResponse, topicsResponse, + countriesResponse, ] = await Future.wait([ _headlinesRepository.read(id: _headlineId), _sourcesRepository.readAll( @@ -57,11 +59,15 @@ class EditHeadlineBloc extends Bloc { _topicsRepository.readAll( sort: [const SortOption('updatedAt', SortOrder.desc)], ), + _countriesRepository.readAll( + sort: [const SortOption('name', SortOrder.asc)], + ), ]); final headline = headlineResponse as Headline; final sources = (sourcesResponse as PaginatedResponse).items; final topics = (topicsResponse as PaginatedResponse).items; + final countries = (countriesResponse as PaginatedResponse).items; emit( state.copyWith( @@ -76,6 +82,7 @@ class EditHeadlineBloc extends Bloc { eventCountry: () => headline.eventCountry, sources: sources, topics: topics, + countries: countries, contentStatus: headline.status, ), ); @@ -159,16 +166,12 @@ class EditHeadlineBloc extends Bloc { EditHeadlineCountryChanged event, Emitter emit, ) { - final packageCountry = event.country; - if (packageCountry == null) { - emit(state.copyWith(eventCountry: () => null)); - } else { - final coreCountry = adaptPackageCountryToCoreCountry(packageCountry); - emit( - state.copyWith( - eventCountry: () => coreCountry, status: EditHeadlineStatus.initial), - ); - } + emit( + state.copyWith( + eventCountry: () => event.country, + status: EditHeadlineStatus.initial, + ), + ); } void _onStatusChanged( diff --git a/lib/content_management/bloc/edit_headline/edit_headline_event.dart b/lib/content_management/bloc/edit_headline/edit_headline_event.dart index 933ae8f..6bad7bd 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_event.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_event.dart @@ -64,7 +64,7 @@ final class EditHeadlineTopicChanged extends EditHeadlineEvent { /// Event for when the headline's country is changed. final class EditHeadlineCountryChanged extends EditHeadlineEvent { const EditHeadlineCountryChanged(this.country); - final picker.Country? country; + final Country? country; @override List get props => [country]; } 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 024c230..fba983f 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_state.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_state.dart @@ -32,6 +32,7 @@ final class EditHeadlineState extends Equatable { this.eventCountry, this.sources = const [], this.topics = const [], + this.countries = const [], this.contentStatus = ContentStatus.active, this.exception, this.updatedHeadline, @@ -48,6 +49,7 @@ final class EditHeadlineState extends Equatable { final Country? eventCountry; final List sources; final List topics; + final List countries; final ContentStatus contentStatus; final HttpException? exception; final Headline? updatedHeadline; @@ -74,6 +76,7 @@ final class EditHeadlineState extends Equatable { ValueGetter? eventCountry, List? sources, List? topics, + List? countries, ContentStatus? contentStatus, HttpException? exception, Headline? updatedHeadline, @@ -90,6 +93,7 @@ final class EditHeadlineState extends Equatable { eventCountry: eventCountry != null ? eventCountry() : this.eventCountry, sources: sources ?? this.sources, topics: topics ?? this.topics, + countries: countries ?? this.countries, contentStatus: contentStatus ?? this.contentStatus, exception: exception, updatedHeadline: updatedHeadline ?? this.updatedHeadline, @@ -109,6 +113,7 @@ final class EditHeadlineState extends Equatable { eventCountry, sources, topics, + countries, contentStatus, exception, updatedHeadline, 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 5fa3322..fb652af 100644 --- a/lib/content_management/bloc/edit_source/edit_source_bloc.dart +++ b/lib/content_management/bloc/edit_source/edit_source_bloc.dart @@ -1,12 +1,8 @@ import 'package:bloc/bloc.dart'; import 'package:core/core.dart'; -import 'package:country_picker/country_picker.dart' as picker; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.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/shared/shared.dart'; -import 'package:language_picker/languages.dart'; part 'edit_source_event.dart'; part 'edit_source_state.dart'; @@ -16,10 +12,14 @@ class EditSourceBloc extends Bloc { /// {@macro edit_source_bloc} EditSourceBloc({ required DataRepository sourcesRepository, + required DataRepository countriesRepository, + required DataRepository languagesRepository, required String sourceId, - }) : _sourcesRepository = sourcesRepository, - _sourceId = sourceId, - super(const EditSourceState()) { + }) : _sourcesRepository = sourcesRepository, + _countriesRepository = countriesRepository, + _languagesRepository = languagesRepository, + _sourceId = sourceId, + super(const EditSourceState()) { on(_onLoaded); on(_onNameChanged); on(_onDescriptionChanged); @@ -32,6 +32,8 @@ class EditSourceBloc extends Bloc { } final DataRepository _sourcesRepository; + final DataRepository _countriesRepository; + final DataRepository _languagesRepository; final String _sourceId; Future _onLoaded( @@ -40,7 +42,32 @@ class EditSourceBloc extends Bloc { ) async { emit(state.copyWith(status: EditSourceStatus.loading)); try { - final source = await _sourcesRepository.read(id: _sourceId); + final [ + sourceResponse, + countriesResponse, + languagesResponse, + ] = await Future.wait([ + _sourcesRepository.read(id: _sourceId), + _countriesRepository.readAll( + sort: [const SortOption('name', SortOrder.asc)], + ), + _languagesRepository.readAll( + sort: [const SortOption('name', SortOrder.asc)], + ), + ]); + + final source = sourceResponse as Source; + final countries = (countriesResponse as PaginatedResponse).items; + final languages = (languagesResponse as PaginatedResponse).items; + + // The source contains a Language object. We need to find the equivalent + // object in the full list of languages to ensure the DropdownButton + // can correctly identify and display the initial selection by reference. + final selectedLanguage = languages.firstWhere( + (listLanguage) => listLanguage == source.language, + orElse: () => source.language, + ); + emit( state.copyWith( status: EditSourceStatus.initial, @@ -49,11 +76,11 @@ class EditSourceBloc extends Bloc { description: source.description, url: source.url, sourceType: () => source.sourceType, - language: () => adaptLanguageCodeToPackageLanguage( - source.language, - ), + language: () => selectedLanguage, headquarters: () => source.headquarters, contentStatus: source.status, + countries: countries, + languages: languages, ), ); } on HttpException catch (e) { @@ -122,16 +149,12 @@ class EditSourceBloc extends Bloc { EditSourceHeadquartersChanged event, Emitter emit, ) { - final packageCountry = event.headquarters; - if (packageCountry == null) { - emit(state.copyWith(headquarters: () => null)); - } else { - final coreCountry = adaptPackageCountryToCoreCountry(packageCountry); - emit( - state.copyWith( - headquarters: () => coreCountry, status: EditSourceStatus.initial), - ); - } + emit( + state.copyWith( + headquarters: () => event.headquarters, + status: EditSourceStatus.initial, + ), + ); } void _onStatusChanged( @@ -172,7 +195,7 @@ class EditSourceBloc extends Bloc { description: state.description, url: state.url, sourceType: state.sourceType, - language: adaptPackageLanguageToLanguageCode(state.language!), + language: state.language, headquarters: state.headquarters, status: state.contentStatus, updatedAt: DateTime.now(), @@ -197,33 +220,3 @@ class EditSourceBloc extends Bloc { } } } - -/// Adds localization support to the [SourceType] enum. -extension SourceTypeL10n on SourceType { - /// Returns the localized name for the source type. - /// - /// This requires an [AppLocalizations] instance, which is typically - /// retrieved from the build context. - String localizedName(AppLocalizations l10n) { - switch (this) { - case SourceType.newsAgency: - return l10n.sourceTypeNewsAgency; - case SourceType.localNewsOutlet: - return l10n.sourceTypeLocalNewsOutlet; - case SourceType.nationalNewsOutlet: - return l10n.sourceTypeNationalNewsOutlet; - case SourceType.internationalNewsOutlet: - return l10n.sourceTypeInternationalNewsOutlet; - case SourceType.specializedPublisher: - return l10n.sourceTypeSpecializedPublisher; - case SourceType.blog: - return l10n.sourceTypeBlog; - case SourceType.governmentSource: - return l10n.sourceTypeGovernmentSource; - case SourceType.aggregator: - return l10n.sourceTypeAggregator; - case SourceType.other: - return l10n.sourceTypeOther; - } - } -} diff --git a/lib/content_management/bloc/edit_source/edit_source_event.dart b/lib/content_management/bloc/edit_source/edit_source_event.dart index 2ba8c53..8620d9a 100644 --- a/lib/content_management/bloc/edit_source/edit_source_event.dart +++ b/lib/content_management/bloc/edit_source/edit_source_event.dart @@ -66,7 +66,7 @@ final class EditSourceLanguageChanged extends EditSourceEvent { final class EditSourceHeadquartersChanged extends EditSourceEvent { const EditSourceHeadquartersChanged(this.headquarters); - final picker.Country? headquarters; + final Country? headquarters; @override List get props => [headquarters]; 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 557ef6b..1c65c6a 100644 --- a/lib/content_management/bloc/edit_source/edit_source_state.dart +++ b/lib/content_management/bloc/edit_source/edit_source_state.dart @@ -29,6 +29,8 @@ final class EditSourceState extends Equatable { this.sourceType, this.language, this.headquarters, + this.countries = const [], + this.languages = const [], this.contentStatus = ContentStatus.active, this.exception, this.updatedSource, @@ -42,6 +44,8 @@ final class EditSourceState extends Equatable { final SourceType? sourceType; final Language? language; final Country? headquarters; + final List countries; + final List languages; final ContentStatus contentStatus; final HttpException? exception; final Source? updatedSource; @@ -64,6 +68,8 @@ final class EditSourceState extends Equatable { ValueGetter? sourceType, ValueGetter? language, ValueGetter? headquarters, + List? countries, + List? languages, ContentStatus? contentStatus, HttpException? exception, Source? updatedSource, @@ -77,6 +83,8 @@ final class EditSourceState extends Equatable { sourceType: sourceType != null ? sourceType() : this.sourceType, language: language != null ? language() : this.language, headquarters: headquarters != null ? headquarters() : this.headquarters, + countries: countries ?? this.countries, + languages: languages ?? this.languages, contentStatus: contentStatus ?? this.contentStatus, exception: exception, updatedSource: updatedSource ?? this.updatedSource, @@ -93,6 +101,8 @@ final class EditSourceState extends Equatable { sourceType, language, headquarters, + countries, + languages, contentStatus, exception, updatedSource, diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index ae795cb..204da4e 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -1,5 +1,4 @@ import 'package:core/core.dart'; -import 'package:country_picker/country_picker.dart' as picker; import 'package:data_repository/data_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -25,6 +24,7 @@ class CreateHeadlinePage extends StatelessWidget { headlinesRepository: context.read>(), sourcesRepository: context.read>(), topicsRepository: context.read>(), + countriesRepository: context.read>(), )..add(const CreateHeadlineDataLoaded()), child: const _CreateHeadlineView(), ); @@ -214,18 +214,13 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { .add(CreateHeadlineTopicChanged(value)), ), const SizedBox(height: AppSpacing.lg), - CountryPickerFormField( + CountryDropdownFormField( labelText: l10n.countryName, - initialValue: state.eventCountry != null - ? adaptCoreCountryToPackageCountry( - state.eventCountry!, - ) - : null, - onChanged: (picker.Country country) { - context.read().add( - CreateHeadlineCountryChanged(country), - ); - }, + countries: state.countries, + initialValue: state.eventCountry, + onChanged: (value) => context + .read() + .add(CreateHeadlineCountryChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( diff --git a/lib/content_management/view/create_source_page.dart b/lib/content_management/view/create_source_page.dart index cfee1d7..2de3892 100644 --- a/lib/content_management/view/create_source_page.dart +++ b/lib/content_management/view/create_source_page.dart @@ -1,12 +1,11 @@ import 'package:core/core.dart'; -import 'package:country_picker/country_picker.dart' as picker; import 'package:data_repository/data_repository.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/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/content_management/bloc/edit_source/edit_source_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'; import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -24,6 +23,8 @@ class CreateSourcePage extends StatelessWidget { return BlocProvider( create: (context) => CreateSourceBloc( sourcesRepository: context.read>(), + countriesRepository: context.read>(), + languagesRepository: context.read>(), )..add(const CreateSourceDataLoaded()), child: const _CreateSourceView(), ); @@ -160,13 +161,13 @@ class _CreateSourceViewState extends State<_CreateSourceView> { .add(CreateSourceUrlChanged(value)), ), const SizedBox(height: AppSpacing.lg), - LanguagePickerFormField( + LanguageDropdownFormField( labelText: l10n.language, + languages: state.languages, initialValue: state.language, - onChanged: (language) => - context.read().add( - CreateSourceLanguageChanged(language), - ), + onChanged: (value) => context + .read() + .add(CreateSourceLanguageChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( @@ -189,18 +190,13 @@ class _CreateSourceViewState extends State<_CreateSourceView> { .add(CreateSourceTypeChanged(value)), ), const SizedBox(height: AppSpacing.lg), - CountryPickerFormField( + CountryDropdownFormField( labelText: l10n.headquarters, - initialValue: state.headquarters != null - ? adaptCoreCountryToPackageCountry( - state.headquarters!, - ) - : null, - onChanged: (picker.Country country) { - context.read().add( - CreateSourceHeadquartersChanged(country), - ); - }, + countries: state.countries, + initialValue: state.headquarters, + onChanged: (value) => context + .read() + .add(CreateSourceHeadquartersChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index b2caf30..9387a25 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -1,5 +1,4 @@ import 'package:core/core.dart'; -import 'package:country_picker/country_picker.dart' as picker; import 'package:data_repository/data_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -28,6 +27,7 @@ class EditHeadlinePage extends StatelessWidget { headlinesRepository: context.read>(), sourcesRepository: context.read>(), topicsRepository: context.read>(), + countriesRepository: context.read>(), headlineId: headlineId, )..add(const EditHeadlineLoaded()), child: const _EditHeadlineView(), @@ -178,6 +178,17 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { } } + Country? selectedCountry; + if (state.eventCountry != null) { + try { + selectedCountry = state.countries.firstWhere( + (c) => c.id == state.eventCountry!.id, + ); + } catch (_) { + selectedCountry = null; + } + } + return SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), @@ -271,18 +282,13 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { .add(EditHeadlineTopicChanged(value)), ), const SizedBox(height: AppSpacing.lg), - CountryPickerFormField( + CountryDropdownFormField( labelText: l10n.countryName, - initialValue: state.eventCountry != null - ? adaptCoreCountryToPackageCountry( - state.eventCountry!, - ) - : null, - onChanged: (picker.Country country) { - context.read().add( - EditHeadlineCountryChanged(country), - ); - }, + countries: state.countries, + initialValue: selectedCountry, + onChanged: (value) => context + .read() + .add(EditHeadlineCountryChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( diff --git a/lib/content_management/view/edit_source_page.dart b/lib/content_management/view/edit_source_page.dart index fb20f02..b532b81 100644 --- a/lib/content_management/view/edit_source_page.dart +++ b/lib/content_management/view/edit_source_page.dart @@ -1,11 +1,11 @@ import 'package:core/core.dart'; -import 'package:country_picker/country_picker.dart' as picker; import 'package:data_repository/data_repository.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/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/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'; import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -26,6 +26,8 @@ class EditSourcePage extends StatelessWidget { return BlocProvider( create: (context) => EditSourceBloc( sourcesRepository: context.read>(), + countriesRepository: context.read>(), + languagesRepository: context.read>(), sourceId: sourceId, )..add(const EditSourceLoaded()), child: const _EditSourceView(), @@ -189,13 +191,13 @@ class _EditSourceViewState extends State<_EditSourceView> { ), ), const SizedBox(height: AppSpacing.lg), - LanguagePickerFormField( + LanguageDropdownFormField( labelText: l10n.language, + languages: state.languages, initialValue: state.language, - onChanged: (language) => - context.read().add( - EditSourceLanguageChanged(language), - ), + onChanged: (value) => context + .read() + .add(EditSourceLanguageChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( @@ -218,18 +220,13 @@ class _EditSourceViewState extends State<_EditSourceView> { ), ), const SizedBox(height: AppSpacing.lg), - CountryPickerFormField( + CountryDropdownFormField( labelText: l10n.headquarters, - initialValue: state.headquarters != null - ? adaptCoreCountryToPackageCountry( - state.headquarters!, - ) - : null, - onChanged: (picker.Country country) { - context.read().add( - EditSourceHeadquartersChanged(country), - ); - }, + countries: state.countries, + initialValue: state.headquarters, + onChanged: (value) => context + .read() + .add(EditSourceHeadquartersChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index 1fb9de4..1e60840 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -3,11 +3,11 @@ 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/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/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/router/routes.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/content_status_l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/source_type_l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; diff --git a/lib/shared/extensions/source_type_l10n.dart b/lib/shared/extensions/source_type_l10n.dart new file mode 100644 index 0000000..97fa68a --- /dev/null +++ b/lib/shared/extensions/source_type_l10n.dart @@ -0,0 +1,32 @@ +import 'package:core/core.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; + +/// Adds localization support to the [SourceType] enum. +extension SourceTypeL10n on SourceType { + /// Returns the localized name for the source type. + /// + /// This requires an [AppLocalizations] instance, which is typically + /// retrieved from the build context. + String localizedName(AppLocalizations l10n) { + switch (this) { + case SourceType.newsAgency: + return l10n.sourceTypeNewsAgency; + case SourceType.localNewsOutlet: + return l10n.sourceTypeLocalNewsOutlet; + case SourceType.nationalNewsOutlet: + return l10n.sourceTypeNationalNewsOutlet; + case SourceType.internationalNewsOutlet: + return l10n.sourceTypeInternationalNewsOutlet; + case SourceType.specializedPublisher: + return l10n.sourceTypeSpecializedPublisher; + case SourceType.blog: + return l10n.sourceTypeBlog; + case SourceType.governmentSource: + return l10n.sourceTypeGovernmentSource; + case SourceType.aggregator: + return l10n.sourceTypeAggregator; + case SourceType.other: + return l10n.sourceTypeOther; + } + } +} diff --git a/lib/shared/shared.dart b/lib/shared/shared.dart index 39c64fc..b6d4d45 100644 --- a/lib/shared/shared.dart +++ b/lib/shared/shared.dart @@ -1,3 +1,2 @@ export 'extensions/extensions.dart'; -export 'utils/utils.dart'; export 'widgets/widgets.dart'; diff --git a/lib/shared/utils/country_adapter.dart b/lib/shared/utils/country_adapter.dart deleted file mode 100644 index b0db818..0000000 --- a/lib/shared/utils/country_adapter.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:core/core.dart' as core; -import 'package:country_picker/country_picker.dart' as picker; - -/// Adapts a [picker.Country] from the `country_picker` package to a -/// [core.Country] model. -/// -/// This is used when a user selects a country from the picker and we need to -/// update our application state with our internal [core.Country] model. -core.Country adaptPackageCountryToCoreCountry(picker.Country packageCountry) { - final now = DateTime.now(); - return core.Country( - // Use the ISO code as a unique, deterministic ID, since we are no longer - // fetching countries as entities from a database. - id: packageCountry.countryCode, - isoCode: packageCountry.countryCode, - name: packageCountry.name, - // Construct a flag URL from a public CDN using the ISO code. - flagUrl: - 'https://flagcdn.com/h40/${packageCountry.countryCode.toLowerCase()}.png', - createdAt: now, - updatedAt: now, - status: core.ContentStatus.active, - ); -} - -/// Adapts a [core.Country] model to a [picker.Country] from the -/// `country_picker` package. -/// -/// This is used when we have an existing [core.Country] (e.g., when editing a -/// headline) and need to display it in the UI using the `country_picker` -/// package's widgets. -picker.Country adaptCoreCountryToPackageCountry(core.Country coreCountry) { - return picker.Country.parse(coreCountry.isoCode); -} diff --git a/lib/shared/utils/language_adapter.dart b/lib/shared/utils/language_adapter.dart deleted file mode 100644 index 7a237ef..0000000 --- a/lib/shared/utils/language_adapter.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:language_picker/languages.dart'; - -/// Adapts a [Language] object from the `language_picker` package to a -/// language code string (e.g., 'en', 'ar'). -/// -/// This is used to convert the selected language from the UI picker into the -/// format expected by the `Source` model in the core package. -String adaptPackageLanguageToLanguageCode(Language language) { - return language.isoCode; -} - -/// Adapts a language code string (e.g., 'en', 'ar') to a [Language] object -/// from the `language_picker` package. -/// -/// This is used to convert the language code from a `Source` model into an -/// object that can be used to set the initial value of the language picker. -/// -/// Returns `null` if the language code is not found. -Language? adaptLanguageCodeToPackageLanguage(String languageCode) { - try { - return Languages.defaultLanguages.firstWhere( - (lang) => lang.isoCode.toLowerCase() == languageCode.toLowerCase(), - ); - } catch (_) { - // Return null if no matching language is found - return null; - } -} diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart deleted file mode 100644 index e071a4b..0000000 --- a/lib/shared/utils/utils.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'country_adapter.dart'; -export 'language_adapter.dart'; diff --git a/lib/shared/widgets/country_dropdown_form_field.dart b/lib/shared/widgets/country_dropdown_form_field.dart new file mode 100644 index 0000000..edcb47d --- /dev/null +++ b/lib/shared/widgets/country_dropdown_form_field.dart @@ -0,0 +1,47 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// A reusable dropdown form field for selecting a country. +class CountryDropdownFormField extends StatelessWidget { + /// {@macro country_dropdown_form_field} + const CountryDropdownFormField({ + required this.countries, + required this.onChanged, + this.initialValue, + this.labelText, + super.key, + }); + + /// The list of countries to display in the dropdown. + final List countries; + + /// The currently selected country. + final Country? initialValue; + + /// The callback that is called when the user selects a country. + final ValueChanged onChanged; + + /// The text to display as the label for the form field. + final String? labelText; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + + return DropdownButtonFormField( + value: initialValue, + decoration: InputDecoration( + labelText: labelText ?? l10n.countryName, + border: const OutlineInputBorder(), + ), + items: [ + DropdownMenuItem(value: null, child: Text(l10n.none)), + ...countries.map( + (c) => DropdownMenuItem(value: c, child: Text(c.name)), + ), + ], + onChanged: onChanged, + ); + } +} diff --git a/lib/shared/widgets/country_picker_form_field.dart b/lib/shared/widgets/country_picker_form_field.dart deleted file mode 100644 index d9be519..0000000 --- a/lib/shared/widgets/country_picker_form_field.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:country_picker/country_picker.dart' as picker; -import 'package:flutter/material.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:ui_kit/ui_kit.dart'; - -/// A form field for selecting a country using the `country_picker` package. -class CountryPickerFormField extends StatelessWidget { - /// Creates a [CountryPickerFormField]. - const CountryPickerFormField({ - required this.onChanged, - this.initialValue, - this.labelText, - super.key, - }); - - /// The currently selected country. Can be null. - final picker.Country? initialValue; - - /// Callback function that is called when a new country is selected. - final ValueChanged onChanged; - - /// The text to display as the label for the form field. - final String? labelText; - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - return InkWell( - onTap: () { - picker.showCountryPicker( - context: context, - onSelect: onChanged, - countryListTheme: picker.CountryListThemeData( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - textStyle: Theme.of(context).textTheme.bodyMedium, - bottomSheetHeight: MediaQuery.of(context).size.height * 0.8, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(AppSpacing.md), - topRight: Radius.circular(AppSpacing.md), - ), - inputDecoration: InputDecoration( - labelText: l10n.countryPickerSearchLabel, - hintText: l10n.countryPickerSearchHint, - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary.withOpacity(0.2), - ), - ), - ), - ), - ); - }, - child: InputDecorator( - decoration: InputDecoration( - labelText: labelText, - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.md, - ), - ), - child: Row( - children: [ - if (initialValue != null) ...[ - Text(initialValue!.flagEmoji, style: const TextStyle(fontSize: 24)), - const SizedBox(width: AppSpacing.md), - Expanded( - child: Text( - initialValue!.name, - style: Theme.of(context).textTheme.bodyLarge, - overflow: TextOverflow.ellipsis, - ), - ), - ] else - Expanded( - child: Text( - l10n.countryPickerSelectCountryLabel, - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: Theme.of(context).hintColor), - ), - ), - const Icon(Icons.arrow_drop_down), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/shared/widgets/language_dropdown_form_field.dart b/lib/shared/widgets/language_dropdown_form_field.dart new file mode 100644 index 0000000..dc8e1d4 --- /dev/null +++ b/lib/shared/widgets/language_dropdown_form_field.dart @@ -0,0 +1,47 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// A reusable dropdown form field for selecting a language. +class LanguageDropdownFormField extends StatelessWidget { + /// {@macro language_dropdown_form_field} + const LanguageDropdownFormField({ + required this.languages, + required this.onChanged, + this.initialValue, + this.labelText, + super.key, + }); + + /// The list of languages to display in the dropdown. + final List languages; + + /// The currently selected language. + final Language? initialValue; + + /// The callback that is called when the user selects a language. + final ValueChanged onChanged; + + /// The text to display as the label for the form field. + final String? labelText; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + + return DropdownButtonFormField( + value: initialValue, + decoration: InputDecoration( + labelText: labelText ?? l10n.language, + border: const OutlineInputBorder(), + ), + items: [ + DropdownMenuItem(value: null, child: Text(l10n.none)), + ...languages.map( + (l) => DropdownMenuItem(value: l, child: Text(l.name)), + ), + ], + onChanged: onChanged, + ); + } +} diff --git a/lib/shared/widgets/language_picker_form_field.dart b/lib/shared/widgets/language_picker_form_field.dart deleted file mode 100644 index 1a512d3..0000000 --- a/lib/shared/widgets/language_picker_form_field.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:language_picker/language_picker.dart'; -import 'package:language_picker/languages.dart'; - -/// A form field for selecting a language using the `language_picker` package. -/// -/// This widget wraps the language picker functionality in a standard -/// [FormField], making it easy to integrate into forms for validation -/// and state management. It presents as a read-only [TextFormField] that, -/// when tapped, opens a language selection dialog. -class LanguagePickerFormField extends FormField { - /// Creates a [LanguagePickerFormField]. - /// - /// The [onSaved], [validator], [initialValue], and [autovalidateMode] are - /// standard [FormField] properties. - /// - /// The [labelText] is displayed as the input field's label. - /// The [onChanged] callback is invoked when a new language is selected. - LanguagePickerFormField({ - super.key, - super.onSaved, - super.validator, - super.initialValue, - super.autovalidateMode, - String? labelText, - void Function(Language)? onChanged, - }) : super( - builder: (FormFieldState state) { - // This controller is just for displaying the text. The actual - // value is managed by the FormField's state. - final controller = TextEditingController( - text: state.value?.name, - ); - - // Helper to build a simple list item for the dialog. - Widget buildDialogItem(Language language) => Row( - children: [ - Text(language.name), - const SizedBox(width: 8), - Flexible(child: Text('(${language.isoCode})')), - ], - ); - - void openLanguagePickerDialog() { - showDialog( - context: state.context, - builder: (context) => LanguagePickerDialog( - isSearchable: true, - title: const Text('Select your language'), - onValuePicked: (Language language) { - state.didChange(language); - onChanged?.call(language); - controller.text = language.name; - }, - itemBuilder: buildDialogItem, - ), - ); - } - - return TextFormField( - controller: controller, - readOnly: true, - decoration: InputDecoration( - labelText: labelText ?? 'Language', - border: const OutlineInputBorder(), - errorText: state.errorText, - suffixIcon: const Icon(Icons.arrow_drop_down), - ), - onTap: openLanguagePickerDialog, - ); - }, - ); -} \ No newline at end of file diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart index f2917f7..c108713 100644 --- a/lib/shared/widgets/widgets.dart +++ b/lib/shared/widgets/widgets.dart @@ -1,2 +1,2 @@ -export 'country_picker_form_field.dart'; -export 'language_picker_form_field.dart'; +export 'country_dropdown_form_field.dart'; +export 'language_dropdown_form_field.dart'; diff --git a/pubspec.lock b/pubspec.lock index 835bcd4..9d34443 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -90,18 +90,10 @@ packages: description: path: "." ref: HEAD - resolved-ref: "3022edcb06d7c55ea81f25126b7f8c28c9105d4c" + resolved-ref: "7a1b54f8c0f94f6c04af3404c8110181e080b5c4" url: "https://github.com/flutter-news-app-full-source-code/core.git" source: git version: "0.0.0" - country_picker: - dependency: "direct main" - description: - name: country_picker - sha256: "9b14c04f9a35e99f6de6bcbc453a556bb98345aecb481c7a0e843c94c2bee1f8" - url: "https://pub.dev" - source: hosted - version: "2.0.27" crypto: dependency: transitive description: @@ -348,14 +340,6 @@ packages: url: "https://github.com/flutter-news-app-full-source-code/kv-storage-shared-preferences.git" source: git version: "0.0.0" - language_picker: - dependency: "direct main" - description: - name: language_picker - sha256: cace0eab53b712e26f5d2cd834a050b6dd6ab56b2ba31b3000dbe5f89f33f5fd - url: "https://pub.dev" - source: hosted - version: "0.4.5" logging: dependency: "direct main" description: @@ -476,14 +460,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5" - recase: - dependency: transitive - description: - name: recase - sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 - url: "https://pub.dev" - source: hosted - version: "4.1.0" shared_preferences: dependency: transitive description: @@ -610,14 +586,6 @@ packages: url: "https://github.com/flutter-news-app-full-source-code/ui-kit.git" source: git version: "0.0.0" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" - url: "https://pub.dev" - source: hosted - version: "2.2.2" universal_platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2b893c8..173d062 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,6 @@ dependencies: core: git: url: https://github.com/flutter-news-app-full-source-code/core.git - country_picker: ^2.0.27 data_api: git: url: https://github.com/flutter-news-app-full-source-code/data-api.git @@ -59,7 +58,6 @@ dependencies: kv_storage_shared_preferences: git: url: https://github.com/flutter-news-app-full-source-code/kv-storage-shared-preferences.git - language_picker: ^0.4.5 logging: ^1.3.0 pinput: ^5.0.1 timeago: ^3.7.1