From 10daa15c7e720d1d3015aee55fa96eb554952192 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Tue, 6 Jun 2023 21:39:58 +0800 Subject: [PATCH] Redesign language settings --- app/lib/controller/pref_controller.dart | 35 ++++++ app/lib/event/event.dart | 2 - app/lib/language_util.dart | 20 ++-- app/lib/stream_util.dart | 13 +++ app/lib/widget/fancy_option_picker.dart | 78 +++++++++---- app/lib/widget/my_app.dart | 83 +++++++------ app/lib/widget/settings.dart | 52 +++------ app/lib/widget/settings/language/bloc.dart | 60 ++++++++++ .../widget/settings/language/state_event.dart | 58 +++++++++ .../widget/settings/language_settings.dart | 110 ++++++++++++++++++ .../widget/settings/language_settings.g.dart | 78 +++++++++++++ 11 files changed, 475 insertions(+), 114 deletions(-) create mode 100644 app/lib/stream_util.dart create mode 100644 app/lib/widget/settings/language/bloc.dart create mode 100644 app/lib/widget/settings/language/state_event.dart create mode 100644 app/lib/widget/settings/language_settings.dart create mode 100644 app/lib/widget/settings/language_settings.g.dart diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index aa36a6cf..39d1646c 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -1,5 +1,7 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/language_util.dart' as language_util; +import 'package:nc_photos/lazy.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:rxdart/rxdart.dart'; @@ -9,6 +11,23 @@ part 'pref_controller.g.dart'; class PrefController { PrefController(this._c); + ValueStream get language => _languageStream(); + + Future setAppLanguage(language_util.AppLanguage value) async { + final backup = _languageController.value; + _languageController.add(value.langId); + try { + if (!await _c.pref.setLanguage(value.langId)) { + throw StateError("Unknown error"); + } + } catch (e, stackTrace) { + _log.severe("[setAppLanguage] Failed setting preference", e, stackTrace); + _languageController + ..addError(e, stackTrace) + ..add(backup); + } + } + ValueStream get albumBrowserZoomLevel => _albumBrowserZoomLevelController.stream; @@ -46,7 +65,23 @@ class PrefController { } } + language_util.AppLanguage _langIdToAppLanguage(int langId) { + try { + return language_util.supportedLanguages[langId]!; + } catch (_) { + return language_util.supportedLanguages[0]!; + } + } + final DiContainer _c; + late final _languageController = + BehaviorSubject.seeded(_c.pref.getLanguageOr(0)); + late final _languageStream = Lazy( + () => _languageController + .map(_langIdToAppLanguage) + .publishValueSeeded(_langIdToAppLanguage(_languageController.value)) + ..connect(), + ); late final _albumBrowserZoomLevelController = BehaviorSubject.seeded(_c.pref.getAlbumBrowserZoomLevelOr(0)); late final _homeAlbumsSortController = diff --git a/app/lib/event/event.dart b/app/lib/event/event.dart index 3e281cb5..eb3abfa8 100644 --- a/app/lib/event/event.dart +++ b/app/lib/event/event.dart @@ -119,8 +119,6 @@ class FavoriteResyncedEvent { class ThemeChangedEvent {} -class LanguageChangedEvent {} - enum MetadataTaskState { /// No work is being done idle, diff --git a/app/lib/language_util.dart b/app/lib/language_util.dart index 2fe5cc35..9f99b7f9 100644 --- a/app/lib/language_util.dart +++ b/app/lib/language_util.dart @@ -23,16 +23,7 @@ class AppLanguage { final Locale? locale; } -AppLanguage getSelectedLanguage() { - try { - final lang = Pref().getLanguageOr(0); - return supportedLanguages[lang]!; - } catch (_) { - return supportedLanguages[_AppLanguageEnum.systemDefault.index]!; - } -} - -Locale? getSelectedLocale() => getSelectedLanguage().locale; +Locale? getSelectedLocale() => _getSelectedLanguage().locale; final supportedLanguages = { _AppLanguageEnum.systemDefault.index: AppLanguage( @@ -89,3 +80,12 @@ enum _AppLanguageEnum { chineseHans, chineseHant, } + +AppLanguage _getSelectedLanguage() { + try { + final lang = Pref().getLanguageOr(0); + return supportedLanguages[lang]!; + } catch (_) { + return supportedLanguages[_AppLanguageEnum.systemDefault.index]!; + } +} diff --git a/app/lib/stream_util.dart b/app/lib/stream_util.dart new file mode 100644 index 00000000..199bf5bc --- /dev/null +++ b/app/lib/stream_util.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; + +class ValueStreamBuilder extends StreamBuilder { + ValueStreamBuilder({ + super.key, + ValueStream? stream, + required super.builder, + }) : super( + stream: stream, + initialData: stream?.value, + ); +} diff --git a/app/lib/widget/fancy_option_picker.dart b/app/lib/widget/fancy_option_picker.dart index bdfa4909..5cc1f987 100644 --- a/app/lib/widget/fancy_option_picker.dart +++ b/app/lib/widget/fancy_option_picker.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:nc_photos/object_extension.dart'; class FancyOptionPickerItem { - FancyOptionPickerItem({ + const FancyOptionPickerItem({ required this.label, this.description, this.isSelected = false, @@ -10,12 +11,12 @@ class FancyOptionPickerItem { this.dense = false, }); - String label; - String? description; - bool isSelected; - VoidCallback? onSelect; - VoidCallback? onUnselect; - bool dense; + final String label; + final String? description; + final bool isSelected; + final VoidCallback? onSelect; + final VoidCallback? onUnselect; + final bool dense; } /// A fancy looking dialog to pick an option @@ -27,26 +28,17 @@ class FancyOptionPicker extends StatelessWidget { }) : super(key: key); @override - build(BuildContext context) { + Widget build(BuildContext context) { return SimpleDialog( title: title != null ? Text(title!) : null, children: items .map((e) => SimpleDialogOption( - child: ListTile( - leading: Icon( - e.isSelected ? Icons.check : null, - color: Theme.of(context).colorScheme.primary, - ), - title: Text( - e.label, - style: e.isSelected - ? TextStyle( - color: Theme.of(context).colorScheme.primary, - ) - : null, - ), - subtitle: e.description == null ? null : Text(e.description!), - onTap: e.isSelected ? e.onUnselect : e.onSelect, + child: FancyOptionPickerItemView( + label: e.label, + description: e.description, + isSelected: e.isSelected, + onSelect: e.onSelect, + onUnselect: e.onUnselect, dense: e.dense, ), )) @@ -57,3 +49,43 @@ class FancyOptionPicker extends StatelessWidget { final String? title; final List items; } + +class FancyOptionPickerItemView extends StatelessWidget { + const FancyOptionPickerItemView({ + super.key, + required this.label, + this.description, + required this.isSelected, + this.onSelect, + this.onUnselect, + required this.dense, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon( + isSelected ? Icons.check : null, + color: Theme.of(context).colorScheme.primary, + ), + title: Text( + label, + style: isSelected + ? TextStyle( + color: Theme.of(context).colorScheme.primary, + ) + : null, + ), + subtitle: description?.run(Text.new), + onTap: isSelected ? onUnselect : onSelect, + dense: dense, + ); + } + + final String label; + final String? description; + final bool isSelected; + final VoidCallback? onSelect; + final VoidCallback? onUnselect; + final bool dense; +} diff --git a/app/lib/widget/my_app.dart b/app/lib/widget/my_app.dart index 51d86e01..589f2824 100644 --- a/app/lib/widget/my_app.dart +++ b/app/lib/widget/my_app.dart @@ -14,6 +14,7 @@ import 'package:nc_photos/legacy/sign_in.dart' as legacy; import 'package:nc_photos/navigation_manager.dart'; import 'package:nc_photos/pref.dart'; import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/stream_util.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/widget/album_dir_picker.dart'; import 'package:nc_photos/widget/album_importer.dart'; @@ -33,6 +34,7 @@ import 'package:nc_photos/widget/places_browser.dart'; import 'package:nc_photos/widget/result_viewer.dart'; import 'package:nc_photos/widget/root_picker.dart'; import 'package:nc_photos/widget/settings.dart'; +import 'package:nc_photos/widget/settings/language_settings.dart'; import 'package:nc_photos/widget/setup.dart'; import 'package:nc_photos/widget/share_folder_picker.dart'; import 'package:nc_photos/widget/shared_file_viewer.dart'; @@ -91,8 +93,6 @@ class _WrappedAppState extends State<_WrappedApp> NavigationManager().setHandler(this); _themeChangedListener = AppEventListener(_onThemeChangedEvent)..begin(); - _langChangedListener = - AppEventListener(_onLangChangedEvent)..begin(); } @override @@ -104,40 +104,44 @@ class _WrappedAppState extends State<_WrappedApp> themeMode = Pref().isDarkThemeOr(false) ? ThemeMode.dark : ThemeMode.light; } - return MaterialApp( - onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, - theme: buildLightTheme(), - darkTheme: buildDarkTheme(), - themeMode: themeMode, - initialRoute: Splash.routeName, - onGenerateRoute: _onGenerateRoute, - navigatorObservers: [MyApp.routeObserver], - navigatorKey: _navigatorKey, - scaffoldMessengerKey: _scaffoldMessengerKey, - locale: language_util.getSelectedLocale(), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: const [ - // the order here doesn't matter, except for the first one, which must - // be en - Locale("en"), - Locale("el"), - Locale("es"), - Locale("fr"), - Locale("ru"), - Locale("de"), - Locale("cs"), - Locale("fi"), - Locale("pl"), - Locale("pt"), - Locale.fromSubtags(languageCode: "zh", scriptCode: "Hans"), - Locale.fromSubtags(languageCode: "zh", scriptCode: "Hant"), - ], - builder: (context, child) { - MyApp._globalContext = context; - return child!; - }, - debugShowCheckedModeBanner: false, - scrollBehavior: const _MyScrollBehavior(), + final prefController = context.read(); + return ValueStreamBuilder( + stream: prefController.language, + builder: (context, snapshot) => MaterialApp( + onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, + theme: buildLightTheme(), + darkTheme: buildDarkTheme(), + themeMode: themeMode, + initialRoute: Splash.routeName, + onGenerateRoute: _onGenerateRoute, + navigatorObservers: [MyApp.routeObserver], + navigatorKey: _navigatorKey, + scaffoldMessengerKey: _scaffoldMessengerKey, + locale: snapshot.requireData.locale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: const [ + // the order here doesn't matter, except for the first one, which must + // be en + Locale("en"), + Locale("el"), + Locale("es"), + Locale("fr"), + Locale("ru"), + Locale("de"), + Locale("cs"), + Locale("fi"), + Locale("pl"), + Locale("pt"), + Locale.fromSubtags(languageCode: "zh", scriptCode: "Hans"), + Locale.fromSubtags(languageCode: "zh", scriptCode: "Hant"), + ], + builder: (context, child) { + MyApp._globalContext = context; + return child!; + }, + debugShowCheckedModeBanner: false, + scrollBehavior: const _MyScrollBehavior(), + ), ); } @@ -147,7 +151,6 @@ class _WrappedAppState extends State<_WrappedApp> SnackBarManager().unregisterHandler(this); NavigationManager().unsetHandler(this); _themeChangedListener.end(); - _langChangedListener.end(); } @override @@ -171,6 +174,7 @@ class _WrappedAppState extends State<_WrappedApp> builder: (context) => const legacy.SignIn(), ), CollectionPicker.routeName: CollectionPicker.buildRoute, + LanguageSettings.routeName: LanguageSettings.buildRoute, }; Route? _onGenerateRoute(RouteSettings settings) { @@ -211,10 +215,6 @@ class _WrappedAppState extends State<_WrappedApp> setState(() {}); } - void _onLangChangedEvent(LanguageChangedEvent ev) { - setState(() {}); - } - Route? _handleBasicRoute(RouteSettings settings) { for (final e in _getRouter().entries) { if (e.key == settings.name) { @@ -604,7 +604,6 @@ class _WrappedAppState extends State<_WrappedApp> final _navigatorKey = GlobalKey(); late AppEventListener _themeChangedListener; - late AppEventListener _langChangedListener; } class _MyScrollBehavior extends MaterialScrollBehavior { diff --git a/app/lib/widget/settings.dart b/app/lib/widget/settings.dart index d47ea3be..51943706 100644 --- a/app/lib/widget/settings.dart +++ b/app/lib/widget/settings.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; -import 'package:kiwi/kiwi.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; @@ -19,6 +19,7 @@ import 'package:nc_photos/platform/notification.dart'; import 'package:nc_photos/pref.dart'; import 'package:nc_photos/service.dart'; import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/stream_util.dart'; import 'package:nc_photos/url_launcher_util.dart'; import 'package:nc_photos/widget/fancy_option_picker.dart'; import 'package:nc_photos/widget/gps_map.dart'; @@ -27,6 +28,7 @@ import 'package:nc_photos/widget/list_tile_center_leading.dart'; import 'package:nc_photos/widget/root_picker.dart'; import 'package:nc_photos/widget/settings/developer_settings.dart'; import 'package:nc_photos/widget/settings/expert_settings.dart'; +import 'package:nc_photos/widget/settings/language_settings.dart'; import 'package:nc_photos/widget/settings/theme_settings.dart'; import 'package:nc_photos/widget/share_folder_picker.dart'; import 'package:nc_photos/widget/simple_input_dialog.dart'; @@ -104,13 +106,18 @@ class _SettingsState extends State { SliverList( delegate: SliverChildListDelegate( [ - ListTile( - leading: const ListTileCenterLeading( - child: Icon(Icons.translate_outlined), + ValueStreamBuilder( + stream: context.read().language, + builder: (context, snapshot) => ListTile( + leading: const ListTileCenterLeading( + child: Icon(Icons.translate_outlined), + ), + title: Text(L10n.global().settingsLanguageTitle), + subtitle: Text(snapshot.requireData.nativeName), + onTap: () { + Navigator.of(context).pushNamed(LanguageSettings.routeName); + }, ), - title: Text(L10n.global().settingsLanguageTitle), - subtitle: Text(language_util.getSelectedLanguage().nativeName), - onTap: () => _onLanguageTap(context), ), SwitchListTile( title: Text(L10n.global().settingsExifSupportTitle), @@ -270,35 +277,6 @@ class _SettingsState extends State { ); } - void _onLanguageTap(BuildContext context) { - final selected = - Pref().getLanguageOr(language_util.supportedLanguages[0]!.langId); - showDialog( - context: context, - builder: (context) => FancyOptionPicker( - items: language_util.supportedLanguages.values - .map((lang) => FancyOptionPickerItem( - label: lang.nativeName, - description: lang.isoName, - isSelected: lang.langId == selected, - onSelect: () { - _log.info( - "[_onLanguageTap] Set language: ${lang.nativeName}"); - Navigator.of(context).pop(lang.langId); - }, - dense: true, - )) - .toList(), - ), - ).then((value) { - if (value != null) { - Pref().setLanguage(value).then((_) { - KiwiContainer().resolve().fire(LanguageChangedEvent()); - }); - } - }); - } - void _onExifSupportChanged(BuildContext context, bool value) { if (value) { showDialog( diff --git a/app/lib/widget/settings/language/bloc.dart b/app/lib/widget/settings/language/bloc.dart new file mode 100644 index 00000000..4cbf3cb8 --- /dev/null +++ b/app/lib/widget/settings/language/bloc.dart @@ -0,0 +1,60 @@ +part of '../language_settings.dart'; + +@npLog +class _Bloc extends Bloc<_Event, _State> implements BlocTag { + _Bloc({ + required this.prefController, + }) : super(_State.init( + selected: prefController.language.value, + )) { + on<_Init>(_onInit); + on<_SelectLanguage>(_onSelectLanguage); + on<_SetError>(_onSetError); + } + + @override + String get tag => _log.fullName; + + @override + void onError(Object error, StackTrace stackTrace) { + // we need this to prevent onError being triggered recursively + if (!isClosed && !_isHandlingError) { + _isHandlingError = true; + try { + add(_SetError(error, stackTrace)); + } catch (_) {} + _isHandlingError = false; + } + super.onError(error, stackTrace); + } + + Future _onInit(_Init ev, Emitter<_State> emit) { + _log.info(ev); + return emit.forEach( + prefController.language, + onData: (data) => state.copyWith( + selected: data, + ), + onError: (e, stackTrace) { + _log.severe("[_onInit] Uncaught exception", e, stackTrace); + return state.copyWith( + error: ExceptionEvent(e, stackTrace), + ); + }, + ); + } + + void _onSelectLanguage(_SelectLanguage ev, Emitter<_State> emit) { + _log.info(ev); + prefController.setAppLanguage(ev.lang); + } + + void _onSetError(_SetError ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); + } + + final PrefController prefController; + + var _isHandlingError = false; +} diff --git a/app/lib/widget/settings/language/state_event.dart b/app/lib/widget/settings/language/state_event.dart new file mode 100644 index 00000000..d893b6fd --- /dev/null +++ b/app/lib/widget/settings/language/state_event.dart @@ -0,0 +1,58 @@ +part of '../language_settings.dart'; + +@genCopyWith +@toString +class _State { + const _State({ + required this.selected, + this.error, + }); + + factory _State.init({ + required language_util.AppLanguage selected, + }) { + return _State( + selected: selected, + ); + } + + @override + String toString() => _$toString(); + + final language_util.AppLanguage selected; + + final ExceptionEvent? error; +} + +abstract class _Event { + const _Event(); +} + +@toString +class _Init implements _Event { + const _Init(); + + @override + String toString() => _$toString(); +} + +@toString +class _SelectLanguage implements _Event { + const _SelectLanguage(this.lang); + + @override + String toString() => _$toString(); + + final language_util.AppLanguage lang; +} + +@toString +class _SetError implements _Event { + const _SetError(this.error, [this.stackTrace]); + + @override + String toString() => _$toString(); + + final Object error; + final StackTrace? stackTrace; +} diff --git a/app/lib/widget/settings/language_settings.dart b/app/lib/widget/settings/language_settings.dart new file mode 100644 index 00000000..3e6395d5 --- /dev/null +++ b/app/lib/widget/settings/language_settings.dart @@ -0,0 +1,110 @@ +import 'package:copy_with/copy_with.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/bloc_util.dart'; +import 'package:nc_photos/controller/pref_controller.dart'; +import 'package:nc_photos/exception_event.dart'; +import 'package:nc_photos/exception_util.dart' as exception_util; +import 'package:nc_photos/k.dart' as k; +import 'package:nc_photos/language_util.dart' as language_util; +import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/widget/fancy_option_picker.dart'; +import 'package:nc_photos/widget/page_visibility_mixin.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:to_string/to_string.dart'; + +part 'language/bloc.dart'; +part 'language/state_event.dart'; +part 'language_settings.g.dart'; + +typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +typedef _BlocListener = BlocListener<_Bloc, _State>; + +class LanguageSettings extends StatelessWidget { + static const routeName = "/language-settings"; + + static Route buildRoute() => MaterialPageRoute( + builder: (_) => const LanguageSettings(), + ); + + const LanguageSettings({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => _Bloc( + prefController: context.read(), + ), + child: const _WrappedLanguageSettings(), + ); + } +} + +class _WrappedLanguageSettings extends StatefulWidget { + const _WrappedLanguageSettings(); + + @override + State createState() => _WrappedLanguageSettingsState(); +} + +class _WrappedLanguageSettingsState extends State<_WrappedLanguageSettings> + with RouteAware, PageVisibilityMixin { + @override + void initState() { + super.initState(); + context.read<_Bloc>().add(const _Init()); + } + + @override + Widget build(BuildContext context) { + return MultiBlocListener( + listeners: [ + _BlocListener( + listenWhen: (previous, current) => previous.error != current.error, + listener: (context, state) { + if (state.error != null && isPageVisible()) { + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(state.error!.error)), + duration: k.snackBarDurationNormal, + )); + } + }, + ), + ], + child: Scaffold( + appBar: AppBar( + title: _BlocBuilder( + buildWhen: (previous, current) => + previous.selected != current.selected, + builder: (context, state) => + Text(L10n.global().settingsLanguageTitle), + ), + ), + body: _BlocBuilder( + buildWhen: (previous, current) => + previous.selected != current.selected, + builder: (context, state) { + final langs = language_util.supportedLanguages.values.toList(); + return ListView.builder( + itemCount: langs.length, + itemBuilder: (context, index) { + final lang = langs[index]; + return FancyOptionPickerItemView( + label: lang.nativeName, + description: lang.isoName, + isSelected: lang.langId == state.selected.langId, + onSelect: () { + context.read<_Bloc>().add(_SelectLanguage(lang)); + }, + dense: true, + ); + }, + ); + }, + ), + ), + ); + } +} diff --git a/app/lib/widget/settings/language_settings.g.dart b/app/lib/widget/settings/language_settings.g.dart new file mode 100644 index 00000000..14514c5d --- /dev/null +++ b/app/lib/widget/settings/language_settings.g.dart @@ -0,0 +1,78 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'language_settings.dart'; + +// ************************************************************************** +// CopyWithLintRuleGenerator +// ************************************************************************** + +// ignore_for_file: library_private_types_in_public_api, duplicate_ignore + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class $_StateCopyWithWorker { + _State call({language_util.AppLanguage? selected, ExceptionEvent? error}); +} + +class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { + _$_StateCopyWithWorkerImpl(this.that); + + @override + _State call({dynamic selected, dynamic error = copyWithNull}) { + return _State( + selected: selected as language_util.AppLanguage? ?? that.selected, + error: error == copyWithNull ? that.error : error as ExceptionEvent?); + } + + final _State that; +} + +extension $_StateCopyWith on _State { + $_StateCopyWithWorker get copyWith => _$copyWith; + $_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this); +} + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$_BlocNpLog on _Bloc { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.settings.language_settings._Bloc"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$_StateToString on _State { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_State {selected: $selected, error: $error}"; + } +} + +extension _$_InitToString on _Init { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_Init {}"; + } +} + +extension _$_SelectLanguageToString on _SelectLanguage { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SelectLanguage {lang: $lang}"; + } +} + +extension _$_SetErrorToString on _SetError { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetError {error: $error, stackTrace: $stackTrace}"; + } +}