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 a6c16cd..9325782 100644 --- a/lib/content_management/bloc/create_source/create_source_bloc.dart +++ b/lib/content_management/bloc/create_source/create_source_bloc.dart @@ -3,8 +3,9 @@ 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/material.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; +import 'package:language_picker/languages.dart'; import 'package:uuid/uuid.dart'; part 'create_source_event.dart'; @@ -72,7 +73,7 @@ class CreateSourceBloc extends Bloc { CreateSourceLanguageChanged event, Emitter emit, ) { - emit(state.copyWith(language: event.language)); + emit(state.copyWith(language: () => event.language)); } void _onHeadquartersChanged( @@ -115,7 +116,7 @@ class CreateSourceBloc extends Bloc { description: state.description, url: state.url, sourceType: state.sourceType!, - language: state.language, + language: adaptPackageLanguageToLanguageCode(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 2750166..8bbf2e7 100644 --- a/lib/content_management/bloc/create_source/create_source_event.dart +++ b/lib/content_management/bloc/create_source/create_source_event.dart @@ -48,7 +48,7 @@ final class CreateSourceTypeChanged extends CreateSourceEvent { /// Event for when the source's language is changed. final class CreateSourceLanguageChanged extends CreateSourceEvent { const CreateSourceLanguageChanged(this.language); - final String language; + final Language? language; @override List get props => [language]; } 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 2d38ce4..66165eb 100644 --- a/lib/content_management/bloc/create_source/create_source_state.dart +++ b/lib/content_management/bloc/create_source/create_source_state.dart @@ -27,7 +27,7 @@ final class CreateSourceState extends Equatable { this.description = '', this.url = '', this.sourceType, - this.language = '', + this.language, this.headquarters, this.contentStatus = ContentStatus.active, this.exception, @@ -39,7 +39,7 @@ final class CreateSourceState extends Equatable { final String description; final String url; final SourceType? sourceType; - final String language; + final Language? language; final Country? headquarters; final ContentStatus contentStatus; final HttpException? exception; @@ -51,7 +51,7 @@ final class CreateSourceState extends Equatable { description.isNotEmpty && url.isNotEmpty && sourceType != null && - language.isNotEmpty && + language != null && headquarters != null; CreateSourceState copyWith({ @@ -60,7 +60,7 @@ final class CreateSourceState extends Equatable { String? description, String? url, ValueGetter? sourceType, - String? language, + ValueGetter? language, ValueGetter? headquarters, ContentStatus? contentStatus, HttpException? exception, @@ -72,7 +72,7 @@ final class CreateSourceState extends Equatable { description: description ?? this.description, url: url ?? this.url, sourceType: sourceType != null ? sourceType() : this.sourceType, - language: language ?? this.language, + language: language != null ? language() : this.language, headquarters: headquarters != null ? headquarters() : this.headquarters, contentStatus: contentStatus ?? this.contentStatus, exception: exception, 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 1a5d629..5fa3322 100644 --- a/lib/content_management/bloc/edit_source/edit_source_bloc.dart +++ b/lib/content_management/bloc/edit_source/edit_source_bloc.dart @@ -6,6 +6,7 @@ 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'; @@ -48,7 +49,9 @@ class EditSourceBloc extends Bloc { description: source.description, url: source.url, sourceType: () => source.sourceType, - language: source.language, + language: () => adaptLanguageCodeToPackageLanguage( + source.language, + ), headquarters: () => source.headquarters, contentStatus: source.status, ), @@ -109,7 +112,7 @@ class EditSourceBloc extends Bloc { ) { emit( state.copyWith( - language: event.language, + language: () => event.language, status: EditSourceStatus.initial, ), ); @@ -169,7 +172,7 @@ class EditSourceBloc extends Bloc { description: state.description, url: state.url, sourceType: state.sourceType, - language: state.language, + language: adaptPackageLanguageToLanguageCode(state.language!), headquarters: state.headquarters, status: state.contentStatus, updatedAt: DateTime.now(), 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 21aa933..2ba8c53 100644 --- a/lib/content_management/bloc/edit_source/edit_source_event.dart +++ b/lib/content_management/bloc/edit_source/edit_source_event.dart @@ -56,8 +56,7 @@ final class EditSourceTypeChanged extends EditSourceEvent { /// Event triggered when the source language input changes. final class EditSourceLanguageChanged extends EditSourceEvent { const EditSourceLanguageChanged(this.language); - - final String language; + final Language? language; @override List get props => [language]; 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 d04d311..557ef6b 100644 --- a/lib/content_management/bloc/edit_source/edit_source_state.dart +++ b/lib/content_management/bloc/edit_source/edit_source_state.dart @@ -27,7 +27,7 @@ final class EditSourceState extends Equatable { this.description = '', this.url = '', this.sourceType, - this.language = '', + this.language, this.headquarters, this.contentStatus = ContentStatus.active, this.exception, @@ -40,7 +40,7 @@ final class EditSourceState extends Equatable { final String description; final String url; final SourceType? sourceType; - final String language; + final Language? language; final Country? headquarters; final ContentStatus contentStatus; final HttpException? exception; @@ -52,7 +52,7 @@ final class EditSourceState extends Equatable { description.isNotEmpty && url.isNotEmpty && sourceType != null && - language.isNotEmpty && + language != null && headquarters != null; EditSourceState copyWith({ @@ -62,7 +62,7 @@ final class EditSourceState extends Equatable { String? description, String? url, ValueGetter? sourceType, - String? language, + ValueGetter? language, ValueGetter? headquarters, ContentStatus? contentStatus, HttpException? exception, @@ -75,7 +75,7 @@ final class EditSourceState extends Equatable { description: description ?? this.description, url: url ?? this.url, sourceType: sourceType != null ? sourceType() : this.sourceType, - language: language ?? this.language, + language: language != null ? language() : this.language, headquarters: headquarters != null ? headquarters() : this.headquarters, contentStatus: contentStatus ?? this.contentStatus, exception: exception, diff --git a/lib/content_management/view/create_source_page.dart b/lib/content_management/view/create_source_page.dart index 728b97a..cfee1d7 100644 --- a/lib/content_management/view/create_source_page.dart +++ b/lib/content_management/view/create_source_page.dart @@ -160,15 +160,13 @@ class _CreateSourceViewState extends State<_CreateSourceView> { .add(CreateSourceUrlChanged(value)), ), const SizedBox(height: AppSpacing.lg), - TextFormField( + LanguagePickerFormField( + labelText: l10n.language, initialValue: state.language, - decoration: InputDecoration( - labelText: l10n.language, - border: const OutlineInputBorder(), - ), - onChanged: (value) => context - .read() - .add(CreateSourceLanguageChanged(value)), + onChanged: (language) => + context.read().add( + CreateSourceLanguageChanged(language), + ), ), 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 9bf0dd8..fb20f02 100644 --- a/lib/content_management/view/edit_source_page.dart +++ b/lib/content_management/view/edit_source_page.dart @@ -45,7 +45,6 @@ class _EditSourceViewState extends State<_EditSourceView> { late final TextEditingController _nameController; late final TextEditingController _descriptionController; late final TextEditingController _urlController; - late final TextEditingController _languageController; @override void initState() { @@ -54,7 +53,6 @@ class _EditSourceViewState extends State<_EditSourceView> { _nameController = TextEditingController(text: state.name); _descriptionController = TextEditingController(text: state.description); _urlController = TextEditingController(text: state.url); - _languageController = TextEditingController(text: state.language); } @override @@ -62,7 +60,6 @@ class _EditSourceViewState extends State<_EditSourceView> { _nameController.dispose(); _descriptionController.dispose(); _urlController.dispose(); - _languageController.dispose(); super.dispose(); } @@ -130,7 +127,6 @@ class _EditSourceViewState extends State<_EditSourceView> { _nameController.text = state.name; _descriptionController.text = state.description; _urlController.text = state.url; - _languageController.text = state.language; } }, builder: (context, state) { @@ -193,15 +189,13 @@ class _EditSourceViewState extends State<_EditSourceView> { ), ), const SizedBox(height: AppSpacing.lg), - TextFormField( - controller: _languageController, - decoration: InputDecoration( - labelText: l10n.language, - border: const OutlineInputBorder(), - ), - onChanged: (value) => context.read().add( - EditSourceLanguageChanged(value), - ), + LanguagePickerFormField( + labelText: l10n.language, + initialValue: state.language, + onChanged: (language) => + context.read().add( + EditSourceLanguageChanged(language), + ), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( diff --git a/lib/shared/utils/language_adapter.dart b/lib/shared/utils/language_adapter.dart new file mode 100644 index 0000000..7a237ef --- /dev/null +++ b/lib/shared/utils/language_adapter.dart @@ -0,0 +1,28 @@ +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 index d79c0e8..e071a4b 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -1 +1,2 @@ export 'country_adapter.dart'; +export 'language_adapter.dart'; diff --git a/lib/shared/widgets/language_picker_form_field.dart b/lib/shared/widgets/language_picker_form_field.dart new file mode 100644 index 0000000..1a512d3 --- /dev/null +++ b/lib/shared/widgets/language_picker_form_field.dart @@ -0,0 +1,73 @@ +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 a8dd41f..f2917f7 100644 --- a/lib/shared/widgets/widgets.dart +++ b/lib/shared/widgets/widgets.dart @@ -1 +1,2 @@ export 'country_picker_form_field.dart'; +export 'language_picker_form_field.dart'; diff --git a/pubspec.lock b/pubspec.lock index ac2eca8..835bcd4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -348,6 +348,14 @@ 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: @@ -468,6 +476,14 @@ 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: diff --git a/pubspec.yaml b/pubspec.yaml index 3ebff7c..2b893c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ 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