From 873b15190be09960009327156c9813eeaf7804e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20M=C3=A4gel?= Date: Mon, 18 Mar 2024 09:00:26 +0100 Subject: [PATCH] refactor: Improve initial loading and login routines --- .../screenshot_integration_test.dart | 28 ++--- lib/data/database.dart | 16 ++- lib/main.dart | 98 +++++++++------ .../filter/pages/filter_create_edit_page.dart | 95 +++++++-------- .../filter/pages/filter_list_page.dart | 15 +-- .../filter/states/filter_cubit.dart | 2 +- .../filter/states/filter_list_bloc.dart | 5 +- .../filter/states/filter_list_state.dart | 47 +++++++- lib/presentation/layout/adaptive_layout.dart | 18 +++ lib/presentation/login/pages/login_page.dart | 2 +- .../login/states/login_cubit.dart | 43 ++++--- .../login/states/login_state.dart | 74 +++++++----- .../todo/pages/todo_create_edit_page.dart | 113 ++++++++---------- .../todo/pages/todo_list_page.dart | 13 +- .../filter/states/filter_cubit_test.dart | 14 +-- 15 files changed, 325 insertions(+), 258 deletions(-) diff --git a/integration_test/screenshot_integration_test.dart b/integration_test/screenshot_integration_test.dart index 3aa76a1..b902fe9 100644 --- a/integration_test/screenshot_integration_test.dart +++ b/integration_test/screenshot_integration_test.dart @@ -17,11 +17,9 @@ import 'package:ntodotxt/main.dart'; import 'package:ntodotxt/presentation/drawer/states/drawer_cubit.dart'; import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart'; import 'package:ntodotxt/presentation/filter/states/filter_list_bloc.dart'; -import 'package:ntodotxt/presentation/filter/states/filter_list_event.dart'; -import 'package:ntodotxt/presentation/intro/page/intro_page.dart'; import 'package:ntodotxt/presentation/login/states/login_cubit.dart'; import 'package:ntodotxt/presentation/login/states/login_state.dart' - show LoginLoading, LoginOffline, LoginState, LoginWebDAV; + show LoginLocal, LoginState, LoginWebDAV; import 'package:ntodotxt/presentation/todo_file/todo_file_cubit.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; @@ -116,7 +114,7 @@ class AppTester extends StatelessWidget { create: (BuildContext context) => TodoFileCubit( repository: context.read(), defaultLocalPath: appCacheDir, - )..load(), + ), ), BlocProvider( create: (BuildContext context) => DrawerCubit(), @@ -126,32 +124,22 @@ class AppTester extends StatelessWidget { create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), - )..load(), + ), ), BlocProvider( - create: (BuildContext context) { - return FilterListBloc( - repository: context.read(), - ) - ..add(const FilterListSubscriped()) - ..add(const FilterListSynchronizationRequested()); - }, + create: (BuildContext context) => FilterListBloc( + repository: context.read(), + ), ), ], child: Builder( builder: (BuildContext context) { return BlocBuilder( builder: (BuildContext context, LoginState state) { - if (state is LoginLoading) { - return const InitialApp( - child: LoadingPage(), - ); - } else if (state is LoginOffline || state is LoginWebDAV) { + if (state is LoginLocal || state is LoginWebDAV) { return CoreApp(loginState: state); } else { - return const InitialApp( - child: IntroPage(), - ); + return const InitialApp(); } }, ); diff --git a/lib/data/database.dart b/lib/data/database.dart index 8c4e4e9..e19d47b 100644 --- a/lib/data/database.dart +++ b/lib/data/database.dart @@ -21,7 +21,6 @@ class DatabaseController { } Future close() async { - log.info('Close database $path'); if (_database != null) { await _database!.close(); } @@ -45,9 +44,7 @@ class DatabaseController { log.info('Perform database upgrade'); } }, - onOpen: (Database db) { - log.info('Open database $path'); - }, + onOpen: (Database db) {}, singleInstance: true, ); } @@ -56,7 +53,16 @@ class DatabaseController { abstract class ModelController extends DatabaseController { ModelController(super.path); - Future get database async => await instance; + Future get database async { + log.fine('Access database by $T'); + return await instance; + } + + @override + Future close() async { + log.fine('Close database by $T'); + await super.close(); + } Future> list(); diff --git a/lib/main.dart b/lib/main.dart index 128cdaf..a9a3a9a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,6 +22,7 @@ import 'package:ntodotxt/presentation/drawer/states/drawer_cubit.dart'; import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart'; import 'package:ntodotxt/presentation/filter/states/filter_list_bloc.dart'; import 'package:ntodotxt/presentation/filter/states/filter_list_event.dart'; +import 'package:ntodotxt/presentation/filter/states/filter_list_state.dart'; import 'package:ntodotxt/presentation/filter/states/filter_state.dart'; import 'package:ntodotxt/presentation/intro/page/intro_page.dart'; import 'package:ntodotxt/presentation/login/states/login_cubit.dart'; @@ -129,16 +130,10 @@ class App extends StatelessWidget { builder: (BuildContext context) { return BlocBuilder( builder: (BuildContext context, LoginState state) { - if (state is LoginLoading) { - return const InitialApp( - child: LoadingPage(), - ); - } else if (state is LoginOffline || state is LoginWebDAV) { + if (state is LoginLocal || state is LoginWebDAV) { return CoreApp(loginState: state); } else { - return const InitialApp( - child: IntroPage(), - ); + return const InitialApp(); } }, ); @@ -150,15 +145,32 @@ class App extends StatelessWidget { } class InitialApp extends StatelessWidget { - final Widget child; final ThemeMode? themeMode; const InitialApp({ - required this.child, this.themeMode, super.key, }); + Future _initialize(BuildContext context) async { + if (context.mounted) { + await context.read().load(); + } + if (context.mounted) { + await context.read().load(); + } + if (context.mounted) { + context.read() + ..add(const FilterListSubscriped()) + ..add(const FilterListSynchronizationRequested()); + } + if (context.mounted) { + await context.read().login(); + } + + return true; + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -177,10 +189,14 @@ class InitialApp extends StatelessWidget { ), BlocListener( listener: (BuildContext context, TodoFileState state) { - if (state is TodoFileReady) { - context.read().login(); - } else if (state is TodoFileError) { - context.read().logout(); + if (state is TodoFileError) { + SnackBarHandler.error(context, state.message); + } + }, + ), + BlocListener( + listener: (BuildContext context, FilterListState state) { + if (state is FilterListError) { SnackBarHandler.error(context, state.message); } }, @@ -193,31 +209,45 @@ class InitialApp extends StatelessWidget { }, ), ], - child: child, + child: FutureBuilder( + future: _initialize(context), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return BlocBuilder( + builder: (BuildContext context, LoginState state) { + if (state is LoginLoading || + state is LoginLocal || + state is LoginWebDAV) { + // Keep loading screen to prevent screen flickering. + return _loadingScreen(); + } else { + return const IntroPage(); + } + }, + ); + } else if (snapshot.hasError) { + return _errorScreen(); + } else { + return _loadingScreen(); + } + }, + ), ), ); } -} - -class LoadingPage extends StatelessWidget { - final String message; - - const LoadingPage({ - this.message = 'Loading', - super.key, - }); - @override - Widget build(BuildContext context) { - context.read().load(); - context.read().load(); - context.read() - ..add(const FilterListSubscriped()) - ..add(const FilterListSynchronizationRequested()); + Widget _loadingScreen() { + return const Scaffold( + body: Center( + child: Text('Loading'), + ), + ); + } - return Scaffold( + Widget _errorScreen() { + return const Scaffold( body: Center( - child: Text(message), + child: Text('Something went wrong.'), ), ); } @@ -273,7 +303,7 @@ class CoreApp extends StatelessWidget { '${todoFileState.localPath}${Platform.pathSeparator}${todoFileState.todoFilename}'); log.info('Use todo file ${todoFile.path}'); switch (loginState) { - case LoginOffline(): + case LoginLocal(): log.info('Use local backend'); api = LocalTodoListApi(todoFile: todoFile); case LoginWebDAV(): diff --git a/lib/presentation/filter/pages/filter_create_edit_page.dart b/lib/presentation/filter/pages/filter_create_edit_page.dart index efb2314..42bcb8c 100644 --- a/lib/presentation/filter/pages/filter_create_edit_page.dart +++ b/lib/presentation/filter/pages/filter_create_edit_page.dart @@ -46,65 +46,58 @@ class FilterCreateEditPage extends StatelessWidget { filterRepository: context.read(), filter: initFilter, ), - child: BlocListener( - listener: (BuildContext context, FilterState state) { - if (state is FilterError) { - SnackBarHandler.error(context, state.message); + child: GestureDetector( + onTap: () { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); } }, - child: GestureDetector( - onTap: () { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } - }, - child: Scaffold( - appBar: MainAppBar( - title: createMode ? 'Create' : 'Edit', - toolbar: Row( - children: [ - if (!createMode) const DeleteFilterIconButton(), - if (!narrowView) SaveFilterIconButton(initFilter: initFilter), - ], - ), + child: Scaffold( + appBar: MainAppBar( + title: createMode ? 'Create' : 'Edit', + toolbar: Row( + children: [ + if (!createMode) const DeleteFilterIconButton(), + if (!narrowView) SaveFilterIconButton(initFilter: initFilter), + ], ), - body: ListView( - children: [ - const FilterNameTextField(), - const Divider(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: ListTile( - title: Text( - 'General', - style: Theme.of(context).textTheme.titleSmall, - ), + ), + body: ListView( + children: [ + const FilterNameTextField(), + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ListTile( + title: Text( + 'General', + style: Theme.of(context).textTheme.titleSmall, ), ), - const FilterOrderItem(), - const FilterFilterItem(), - const FilterGroupItem(), - const Divider(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: ListTile( - title: Text( - 'Tags', - style: Theme.of(context).textTheme.titleSmall, - ), + ), + const FilterOrderItem(), + const FilterFilterItem(), + const FilterGroupItem(), + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ListTile( + title: Text( + 'Tags', + style: Theme.of(context).textTheme.titleSmall, ), ), - const FilterPrioritiesItem(), - FilterProjectTagsItem(availableTags: projects), - FilterContextTagsItem(availableTags: contexts), - const SizedBox(height: 16), - ], - ), - floatingActionButton: keyboardIsOpen || !narrowView - ? null - : SaveFilterFABButton(initFilter: initFilter), + ), + const FilterPrioritiesItem(), + FilterProjectTagsItem(availableTags: projects), + FilterContextTagsItem(availableTags: contexts), + const SizedBox(height: 16), + ], ), + floatingActionButton: keyboardIsOpen || !narrowView + ? null + : SaveFilterFABButton(initFilter: initFilter), ), ), ); diff --git a/lib/presentation/filter/pages/filter_list_page.dart b/lib/presentation/filter/pages/filter_list_page.dart index fbbe98c..c0b9d12 100644 --- a/lib/presentation/filter/pages/filter_list_page.dart +++ b/lib/presentation/filter/pages/filter_list_page.dart @@ -7,7 +7,7 @@ import 'package:ntodotxt/common_widgets/scroll_to_top.dart'; import 'package:ntodotxt/constants/app.dart'; import 'package:ntodotxt/domain/filter/filter_model.dart'; import 'package:ntodotxt/domain/todo/todo_model.dart' show Priority; -import 'package:ntodotxt/misc.dart' show PopScopeDrawer, SnackBarHandler; +import 'package:ntodotxt/misc.dart' show PopScopeDrawer; import 'package:ntodotxt/presentation/filter/states/filter_list_bloc.dart'; import 'package:ntodotxt/presentation/filter/states/filter_list_state.dart'; @@ -18,16 +18,9 @@ class FilterListPage extends StatelessWidget { Widget build(BuildContext context) { final bool isNarrowLayout = MediaQuery.of(context).size.width < maxScreenWidthCompact; - return BlocListener( - listener: (BuildContext context, FilterListState state) { - if (state is FilterListError) { - SnackBarHandler.error(context, state.message); - } - }, - child: isNarrowLayout - ? const FilterListViewNarrow() - : const FilterListViewWide(), - ); + return isNarrowLayout + ? const FilterListViewNarrow() + : const FilterListViewWide(); } } diff --git a/lib/presentation/filter/states/filter_cubit.dart b/lib/presentation/filter/states/filter_cubit.dart index 2c52241..d03b5f5 100644 --- a/lib/presentation/filter/states/filter_cubit.dart +++ b/lib/presentation/filter/states/filter_cubit.dart @@ -255,10 +255,10 @@ class FilterCubit extends Cubit { Future resetToDefaults() async { try { const Filter defaultFilter = Filter(); - emit(state.save(filter: defaultFilter)); for (var k in ['order', 'filter', 'group']) { await _settingRepository.delete(key: k); } + emit(state.save(filter: defaultFilter)); } on Exception catch (e) { emit(state.error(message: e.toString())); } diff --git a/lib/presentation/filter/states/filter_list_bloc.dart b/lib/presentation/filter/states/filter_list_bloc.dart index 860a772..0b467bc 100644 --- a/lib/presentation/filter/states/filter_list_bloc.dart +++ b/lib/presentation/filter/states/filter_list_bloc.dart @@ -9,7 +9,7 @@ class FilterListBloc extends Bloc { FilterListBloc({required FilterRepository repository}) : _repository = repository, - super(const FilterListSuccess()) { + super(const FilterListLoading()) { on(_onFilterListSubscriped); on(_onFilterSynchronizationRequested); } @@ -21,7 +21,7 @@ class FilterListBloc extends Bloc { await emit.forEach>( _repository.stream, onData: (filterList) { - return state.success(filterList: filterList); + return state.copyWith(filterList: filterList); }, onError: (e, _) => state.error(message: e.toString()), ); @@ -32,6 +32,7 @@ class FilterListBloc extends Bloc { Emitter emit, ) async { try { + emit(state.loading()); await _repository.refresh(); emit(state.success()); } on Exception catch (e) { diff --git a/lib/presentation/filter/states/filter_list_state.dart b/lib/presentation/filter/states/filter_list_state.dart index b083c33..5202fe6 100644 --- a/lib/presentation/filter/states/filter_list_state.dart +++ b/lib/presentation/filter/states/filter_list_state.dart @@ -8,7 +8,19 @@ sealed class FilterListState extends Equatable { this.filterList = const [], }); - FilterListState success({ + FilterListState copyWith({ + List? filterList, + }); + + FilterListLoading loading({ + List? filterList, + }) { + return FilterListLoading( + filterList: filterList ?? this.filterList, + ); + } + + FilterListSuccess success({ List? filterList, }) { return FilterListSuccess( @@ -16,7 +28,7 @@ sealed class FilterListState extends Equatable { ); } - FilterListState error({ + FilterListError error({ required String message, List? filterList, }) { @@ -27,7 +39,7 @@ sealed class FilterListState extends Equatable { } @override - List get props => [ + List get props => [ filterList, ]; @@ -35,11 +47,28 @@ sealed class FilterListState extends Equatable { String toString() => 'FilterListState { filters: $filterList }'; } +final class FilterListLoading extends FilterListState { + const FilterListLoading({ + super.filterList, + }); + + @override + FilterListLoading copyWith({List? filterList}) => + super.loading(filterList: filterList ?? this.filterList); + + @override + String toString() => 'FilterListLoading { filters: $filterList }'; +} + final class FilterListSuccess extends FilterListState { const FilterListSuccess({ super.filterList, }); + @override + FilterListSuccess copyWith({List? filterList}) => + super.success(filterList: filterList ?? this.filterList); + @override String toString() => 'FilterListSuccess { filters: $filterList }'; } @@ -53,7 +82,17 @@ final class FilterListError extends FilterListState { }); @override - List get props => [ + FilterListError copyWith({ + String? message, + List? filterList, + }) => + super.error( + message: message ?? this.message, + filterList: filterList ?? this.filterList, + ); + + @override + List get props => [ message, filterList, ]; diff --git a/lib/presentation/layout/adaptive_layout.dart b/lib/presentation/layout/adaptive_layout.dart index 0ddaaf8..e0a076e 100644 --- a/lib/presentation/layout/adaptive_layout.dart +++ b/lib/presentation/layout/adaptive_layout.dart @@ -4,9 +4,13 @@ import 'package:ntodotxt/constants/app.dart'; import 'package:ntodotxt/misc.dart'; import 'package:ntodotxt/presentation/drawer/widgets/drawer.dart'; import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart'; +import 'package:ntodotxt/presentation/filter/states/filter_list_bloc.dart'; +import 'package:ntodotxt/presentation/filter/states/filter_list_state.dart'; import 'package:ntodotxt/presentation/filter/states/filter_state.dart'; import 'package:ntodotxt/presentation/login/states/login_cubit.dart'; import 'package:ntodotxt/presentation/login/states/login_state.dart'; +import 'package:ntodotxt/presentation/todo/states/todo_list_bloc.dart'; +import 'package:ntodotxt/presentation/todo/states/todo_list_state.dart'; import 'package:ntodotxt/presentation/todo_file/todo_file_cubit.dart'; import 'package:ntodotxt/presentation/todo_file/todo_file_state.dart'; @@ -58,6 +62,13 @@ class NotificationWrapper extends StatelessWidget { } }, ), + BlocListener( + listener: (BuildContext context, FilterListState state) { + if (state is FilterListError) { + SnackBarHandler.error(context, state.message); + } + }, + ), BlocListener( listener: (BuildContext context, FilterState state) { if (state is FilterError) { @@ -65,6 +76,13 @@ class NotificationWrapper extends StatelessWidget { } }, ), + BlocListener( + listener: (BuildContext context, TodoListState state) { + if (state is TodoListError) { + SnackBarHandler.error(context, state.message); + } + }, + ), ], child: child, ); diff --git a/lib/presentation/login/pages/login_page.dart b/lib/presentation/login/pages/login_page.dart index 60063ff..09ceda7 100644 --- a/lib/presentation/login/pages/login_page.dart +++ b/lib/presentation/login/pages/login_page.dart @@ -46,7 +46,7 @@ class _LocalLoginViewState extends State { onPressed: () async { try { setState(() => loading = true); - await context.read().loginOffline( + await context.read().loginLocal( todoFile: File( '${state.localPath}${Platform.pathSeparator}${state.todoFilename}'), ); diff --git a/lib/presentation/login/states/login_cubit.dart b/lib/presentation/login/states/login_cubit.dart index 01683d4..0d86b8f 100644 --- a/lib/presentation/login/states/login_cubit.dart +++ b/lib/presentation/login/states/login_cubit.dart @@ -12,27 +12,30 @@ class LoginCubit extends Cubit { Future login() async { try { - emit(const LoginLoading()); + emit(state.loading()); String? backendFromsecureStorage = await secureStorage.read(key: 'backend'); Backend backend; if (backendFromsecureStorage == null) { - return emit(const Logout()); + return emit(state.logout()); } try { backend = Backend.values.byName(backendFromsecureStorage); } on Exception { - return emit(const Logout()); + return emit(state.logout()); } if (backend == Backend.none) { - return emit(const Logout()); + return emit(state.logout()); } - if (backend == Backend.offline) { - return emit(const LoginOffline()); + // @todo: Keep 'offline' for backward compatibility. + if (backend == Backend.local || backend == Backend.offline) { + // @todo: Remove with the next releases. + await secureStorage.write(key: 'backend', value: Backend.local.name); + return emit(state.loginLocal()); } if (backend == Backend.webdav) { String? server = await secureStorage.read(key: 'server'); @@ -44,7 +47,7 @@ class LoginCubit extends Cubit { username != null && password != null) { emit( - LoginWebDAV( + state.loginWebDAV( server: server, baseUrl: baseUrl, username: username, @@ -54,33 +57,29 @@ class LoginCubit extends Cubit { } } } on Exception catch (e) { - emit(LoginError(message: e.toString())); + emit(state.error(message: e.toString())); } } - void loginError(String message) { - emit(LoginError(message: message)); - } - Future logout() async { try { await resetSecureStorage(); - emit(const Logout()); + emit(state.logout()); } on Exception catch (e) { - emit(LoginError(message: e.toString())); + emit(state.error(message: e.toString())); } } - Future loginOffline({ + Future loginLocal({ required todoFile, }) async { try { LocalTodoListApi(todoFile: todoFile); // Check before login. await resetSecureStorage(); - await secureStorage.write(key: 'backend', value: Backend.offline.name); - emit(const LoginOffline()); + await secureStorage.write(key: 'backend', value: Backend.local.name); + emit(state.loginLocal()); } on Exception catch (e) { - emit(LoginError(message: e.toString())); + emit(state.error(message: e.toString())); } } @@ -109,7 +108,7 @@ class LoginCubit extends Cubit { await secureStorage.write(key: 'username', value: username); await secureStorage.write(key: 'password', value: password); emit( - LoginWebDAV( + state.loginWebDAV( server: server, baseUrl: baseUrl, username: username, @@ -117,7 +116,7 @@ class LoginCubit extends Cubit { ), ); } on Exception catch (e) { - emit(LoginError(message: e.toString())); + emit(state.error(message: e.toString())); } } @@ -136,9 +135,9 @@ class LoginCubit extends Cubit { await secureStorage.delete(key: attr); } } - emit(const Logout()); + emit(state.logout()); } on Exception catch (e) { - emit(LoginError(message: e.toString())); + emit(state.error(message: e.toString())); } } } diff --git a/lib/presentation/login/states/login_state.dart b/lib/presentation/login/states/login_state.dart index 576d538..7738d93 100644 --- a/lib/presentation/login/states/login_state.dart +++ b/lib/presentation/login/states/login_state.dart @@ -1,10 +1,10 @@ import 'package:equatable/equatable.dart'; -enum Backend { none, offline, webdav } +// Keep 'offline' for backward compatibility. +enum Backend { none, local, offline, webdav } sealed class LoginState extends Equatable { - /// Backend to use to store todos. - final Backend backend; + final Backend backend; // Backend to use to store todos. const LoginState({ this.backend = Backend.none, @@ -12,6 +12,30 @@ sealed class LoginState extends Equatable { LoginState copyWith(); + LoginLoading loading() => const LoginLoading(); + + Logout logout() => const Logout(); + + LoginLocal loginLocal() => const LoginLocal(); + + LoginWebDAV loginWebDAV({ + required String server, + required String baseUrl, + required String username, + required String password, + }) => + LoginWebDAV( + server: server, + baseUrl: baseUrl, + username: username, + password: password, + ); + + LoginError error({ + required String message, + }) => + LoginError(message: message); + @override List get props => [ backend, @@ -27,9 +51,7 @@ final class LoginLoading extends LoginState { }); @override - LoginLoading copyWith() { - return const LoginLoading(); - } + LoginLoading copyWith() => super.loading(); @override List get props => [ @@ -42,13 +64,11 @@ final class LoginLoading extends LoginState { final class Logout extends LoginState { const Logout({ - super.backend, + super.backend = Backend.none, }); @override - Logout copyWith() { - return const Logout(); - } + Logout copyWith() => super.logout(); @override List get props => [ @@ -59,15 +79,13 @@ final class Logout extends LoginState { String toString() => 'Logout { }'; } -final class LoginOffline extends LoginState { - const LoginOffline({ - super.backend = Backend.offline, +final class LoginLocal extends LoginState { + const LoginLocal({ + super.backend = Backend.local, }); @override - LoginOffline copyWith() { - return const LoginOffline(); - } + LoginLocal copyWith() => super.loginLocal(); @override List get props => [ @@ -75,7 +93,7 @@ final class LoginOffline extends LoginState { ]; @override - String toString() => 'LoginOffline { }'; + String toString() => 'LoginLocal { }'; } final class LoginWebDAV extends LoginState { @@ -105,14 +123,13 @@ final class LoginWebDAV extends LoginState { String? baseUrl, String? username, String? password, - }) { - return LoginWebDAV( - server: server ?? this.server, - baseUrl: baseUrl ?? this.baseUrl, - username: username ?? this.username, - password: password ?? this.password, - ); - } + }) => + super.loginWebDAV( + server: server ?? this.server, + baseUrl: baseUrl ?? this.baseUrl, + username: username ?? this.username, + password: password ?? this.password, + ); @override List get props => [ @@ -138,11 +155,8 @@ final class LoginError extends LoginState { @override LoginError copyWith({ String? message, - }) { - return LoginError( - message: message ?? this.message, - ); - } + }) => + super.error(message: message ?? this.message); @override List get props => [ diff --git a/lib/presentation/todo/pages/todo_create_edit_page.dart b/lib/presentation/todo/pages/todo_create_edit_page.dart index 1eb7a22..b71faa9 100644 --- a/lib/presentation/todo/pages/todo_create_edit_page.dart +++ b/lib/presentation/todo/pages/todo_create_edit_page.dart @@ -43,76 +43,69 @@ class TodoCreateEditPage extends StatelessWidget { create: (BuildContext context) => TodoCubit( todo: initTodo ?? Todo(), ), - child: BlocListener( - listener: (BuildContext context, TodoState state) { - if (state is TodoError) { - SnackBarHandler.error(context, state.message); + child: GestureDetector( + onTap: () { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); } }, - child: GestureDetector( - onTap: () { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } - }, - child: Scaffold( - appBar: MainAppBar( - title: createMode ? 'Create' : 'Edit', - toolbar: Row( - children: [ - if (!createMode) const DeleteTodoIconButton(), - if (!narrowView) SaveTodoIconButton(initTodo: initTodo), - ], - ), + child: Scaffold( + appBar: MainAppBar( + title: createMode ? 'Create' : 'Edit', + toolbar: Row( + children: [ + if (!createMode) const DeleteTodoIconButton(), + if (!narrowView) SaveTodoIconButton(initTodo: initTodo), + ], ), - body: ListView( - children: [ - const TodoStringTextField(), - const Divider(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: ListTile( - title: Text( - 'General', - style: Theme.of(context).textTheme.titleSmall, - ), + ), + body: ListView( + children: [ + const TodoStringTextField(), + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ListTile( + title: Text( + 'General', + style: Theme.of(context).textTheme.titleSmall, ), ), - const TodoPriorityItem(), - const Divider(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: ListTile( - title: Text( - 'Dates', - style: Theme.of(context).textTheme.titleSmall, - ), + ), + const TodoPriorityItem(), + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ListTile( + title: Text( + 'Dates', + style: Theme.of(context).textTheme.titleSmall, ), ), - const TodoCreationDateItem(), - if (!createMode) const TodoCompletionDateItem(), - const TodoDueDateItem(), - const Divider(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: ListTile( - title: Text( - 'Tags', - style: Theme.of(context).textTheme.titleSmall, - ), + ), + const TodoCreationDateItem(), + if (!createMode) const TodoCompletionDateItem(), + const TodoDueDateItem(), + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ListTile( + title: Text( + 'Tags', + style: Theme.of(context).textTheme.titleSmall, ), ), - TodoProjectTagsItem(availableTags: projects), - TodoContextTagsItem(availableTags: contexts), - TodoKeyValueTagsItem(availableTags: keyValues), - const SizedBox(height: 16), - ], - ), - floatingActionButton: keyboardIsOpen || !narrowView - ? null - : SaveTodoFABButton(initTodo: initTodo), + ), + TodoProjectTagsItem(availableTags: projects), + TodoContextTagsItem(availableTags: contexts), + TodoKeyValueTagsItem(availableTags: keyValues), + const SizedBox(height: 16), + ], ), + floatingActionButton: keyboardIsOpen || !narrowView + ? null + : SaveTodoFABButton(initTodo: initTodo), ), ), ); diff --git a/lib/presentation/todo/pages/todo_list_page.dart b/lib/presentation/todo/pages/todo_list_page.dart index b25fd3a..9c7cad0 100644 --- a/lib/presentation/todo/pages/todo_list_page.dart +++ b/lib/presentation/todo/pages/todo_list_page.dart @@ -33,16 +33,9 @@ class TodoListPage extends StatelessWidget { Widget _build(BuildContext context) { final bool isNarrowLayout = MediaQuery.of(context).size.width < maxScreenWidthCompact; - return BlocListener( - listener: (BuildContext context, TodoListState state) { - if (state is TodoListError) { - SnackBarHandler.error(context, state.message); - } - }, - child: isNarrowLayout - ? const TodoListViewNarrow() - : const TodoListViewWide(), - ); + return isNarrowLayout + ? const TodoListViewNarrow() + : const TodoListViewWide(); } Widget _buildWithFilter(BuildContext context) { diff --git a/test/presentation/filter/states/filter_cubit_test.dart b/test/presentation/filter/states/filter_cubit_test.dart index 1645777..21b67d7 100644 --- a/test/presentation/filter/states/filter_cubit_test.dart +++ b/test/presentation/filter/states/filter_cubit_test.dart @@ -540,7 +540,7 @@ void main() { filterRepository: FilterRepository(FilterController(databasePath)), filter: origin, ); - cubit.updateDefaultOrder(ListOrder.descending); + await cubit.updateDefaultOrder(ListOrder.descending); expect( cubit.state, @@ -565,7 +565,7 @@ void main() { filterRepository: FilterRepository(FilterController(databasePath)), filter: origin, ); - cubit.updateDefaultFilter(ListFilter.completedOnly); + await cubit.updateDefaultFilter(ListFilter.completedOnly); expect( cubit.state, @@ -590,7 +590,7 @@ void main() { filterRepository: FilterRepository(FilterController(databasePath)), filter: origin, ); - cubit.updateDefaultGroup(ListGroup.priority); + await cubit.updateDefaultGroup(ListGroup.priority); expect( cubit.state, @@ -618,9 +618,9 @@ void main() { filterRepository: FilterRepository(FilterController(databasePath)), filter: origin, ); - cubit.updateDefaultOrder(ListOrder.descending); - cubit.updateDefaultFilter(ListFilter.completedOnly); - cubit.updateDefaultGroup(ListGroup.priority); + await cubit.updateDefaultOrder(ListOrder.descending); + await cubit.updateDefaultFilter(ListFilter.completedOnly); + await cubit.updateDefaultGroup(ListGroup.priority); expect( cubit.state, @@ -638,7 +638,7 @@ void main() { ), ); - cubit.resetToDefaults(); + await cubit.resetToDefaults(); expect( cubit.state, FilterSaved(