Skip to content

Refactor streamline source language selection #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -72,7 +73,7 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
CreateSourceLanguageChanged event,
Emitter<CreateSourceState> emit,
) {
emit(state.copyWith(language: event.language));
emit(state.copyWith(language: () => event.language));
}

void _onHeadquartersChanged(
Expand Down Expand Up @@ -115,7 +116,7 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
description: state.description,
url: state.url,
sourceType: state.sourceType!,
language: state.language,
language: adaptPackageLanguageToLanguageCode(state.language!),
createdAt: now,
updatedAt: now,
headquarters: state.headquarters!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object?> get props => [language];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -51,7 +51,7 @@ final class CreateSourceState extends Equatable {
description.isNotEmpty &&
url.isNotEmpty &&
sourceType != null &&
language.isNotEmpty &&
language != null &&
headquarters != null;

CreateSourceState copyWith({
Expand All @@ -60,7 +60,7 @@ final class CreateSourceState extends Equatable {
String? description,
String? url,
ValueGetter<SourceType?>? sourceType,
String? language,
ValueGetter<Language?>? language,
ValueGetter<Country?>? headquarters,
ContentStatus? contentStatus,
HttpException? exception,
Expand All @@ -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,
Expand Down
9 changes: 6 additions & 3 deletions lib/content_management/bloc/edit_source/edit_source_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -48,7 +49,9 @@ class EditSourceBloc extends Bloc<EditSourceEvent, EditSourceState> {
description: source.description,
url: source.url,
sourceType: () => source.sourceType,
language: source.language,
language: () => adaptLanguageCodeToPackageLanguage(
source.language,
),
headquarters: () => source.headquarters,
contentStatus: source.status,
),
Expand Down Expand Up @@ -109,7 +112,7 @@ class EditSourceBloc extends Bloc<EditSourceEvent, EditSourceState> {
) {
emit(
state.copyWith(
language: event.language,
language: () => event.language,
status: EditSourceStatus.initial,
),
);
Expand Down Expand Up @@ -169,7 +172,7 @@ class EditSourceBloc extends Bloc<EditSourceEvent, EditSourceState> {
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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object?> get props => [language];
Expand Down
10 changes: 5 additions & 5 deletions lib/content_management/bloc/edit_source/edit_source_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -52,7 +52,7 @@ final class EditSourceState extends Equatable {
description.isNotEmpty &&
url.isNotEmpty &&
sourceType != null &&
language.isNotEmpty &&
language != null &&
headquarters != null;

EditSourceState copyWith({
Expand All @@ -62,7 +62,7 @@ final class EditSourceState extends Equatable {
String? description,
String? url,
ValueGetter<SourceType?>? sourceType,
String? language,
ValueGetter<Language?>? language,
ValueGetter<Country?>? headquarters,
ContentStatus? contentStatus,
HttpException? exception,
Expand All @@ -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,
Expand Down
14 changes: 6 additions & 8 deletions lib/content_management/view/create_source_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<CreateSourceBloc>()
.add(CreateSourceLanguageChanged(value)),
onChanged: (language) =>
context.read<CreateSourceBloc>().add(
CreateSourceLanguageChanged(language),
),
),
const SizedBox(height: AppSpacing.lg),
DropdownButtonFormField<SourceType?>(
Expand Down
20 changes: 7 additions & 13 deletions lib/content_management/view/edit_source_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -54,15 +53,13 @@ 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
void dispose() {
_nameController.dispose();
_descriptionController.dispose();
_urlController.dispose();
_languageController.dispose();
super.dispose();
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<EditSourceBloc>().add(
EditSourceLanguageChanged(value),
),
LanguagePickerFormField(
labelText: l10n.language,
initialValue: state.language,
onChanged: (language) =>
context.read<EditSourceBloc>().add(
EditSourceLanguageChanged(language),
),
),
const SizedBox(height: AppSpacing.lg),
DropdownButtonFormField<SourceType?>(
Expand Down
28 changes: 28 additions & 0 deletions lib/shared/utils/language_adapter.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions lib/shared/utils/utils.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export 'country_adapter.dart';
export 'language_adapter.dart';
73 changes: 73 additions & 0 deletions lib/shared/widgets/language_picker_form_field.dart
Original file line number Diff line number Diff line change
@@ -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<Language> {
/// 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<Language> 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: <Widget>[
Text(language.name),
const SizedBox(width: 8),
Flexible(child: Text('(${language.isoCode})')),
],
);

void openLanguagePickerDialog() {
showDialog<void>(
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,
);
},
);
}
1 change: 1 addition & 0 deletions lib/shared/widgets/widgets.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export 'country_picker_form_field.dart';
export 'language_picker_form_field.dart';
16 changes: 16 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading