From 09859c8f45fc5a34acd01fdc5da80aed32023edb Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 30 Mar 2023 15:07:14 +0200 Subject: [PATCH 01/11] upgrade flutter bloc --- lib/main.dart | 49 +- .../blocs/authentication/authentication.dart | 3 - .../authentication/authentication_bloc.dart | 94 ++-- .../authentication/authentication_event.dart | 11 +- .../authentication/authentication_state.dart | 41 +- lib/src/blocs/categories/categories.dart | 3 - lib/src/blocs/categories/categories_bloc.dart | 43 +- .../blocs/categories/categories_event.dart | 6 +- .../blocs/categories/categories_state.dart | 67 ++- lib/src/blocs/login/login.dart | 3 - lib/src/blocs/login/login_bloc.dart | 75 +-- lib/src/blocs/login/login_event.dart | 2 +- lib/src/blocs/login/login_state.dart | 34 +- lib/src/blocs/recipe/recipe.dart | 3 - lib/src/blocs/recipe/recipe_bloc.dart | 99 ++-- lib/src/blocs/recipe/recipe_event.dart | 11 +- lib/src/blocs/recipe/recipe_state.dart | 140 +++--- .../blocs/recipes_short/recipes_short.dart | 3 - .../recipes_short/recipes_short_bloc.dart | 63 ++- .../recipes_short/recipes_short_event.dart | 2 +- .../recipes_short/recipes_short_state.dart | 78 ++- lib/src/screens/category/category_screen.dart | 115 ++--- lib/src/screens/form/login_form.dart | 21 +- lib/src/screens/form/recipe_form.dart | 445 +++++++++--------- lib/src/screens/form/recipe_import_form.dart | 50 +- lib/src/screens/loading_screen.dart | 45 +- lib/src/screens/recipe/recipe_screen.dart | 39 +- lib/src/screens/recipe_create_screen.dart | 10 +- lib/src/screens/recipe_edit_screen.dart | 12 +- lib/src/screens/recipe_import_screen.dart | 10 +- lib/src/screens/recipes_list_screen.dart | 12 +- lib/src/services/authentication_provider.dart | 2 +- lib/src/widget/checkbox_form_field.dart | 1 + lib/src/widget/input/duration_form_field.dart | 8 +- lib/src/widget/input/list_form_field.dart | 2 +- .../input/reorderable_list_form_field.dart | 26 +- pubspec.yaml | 2 +- 37 files changed, 837 insertions(+), 793 deletions(-) delete mode 100644 lib/src/blocs/authentication/authentication.dart delete mode 100644 lib/src/blocs/categories/categories.dart delete mode 100644 lib/src/blocs/login/login.dart delete mode 100644 lib/src/blocs/recipe/recipe.dart delete mode 100644 lib/src/blocs/recipes_short/recipes_short.dart diff --git a/lib/main.dart b/lib/main.dart index fe903ba1..8aee7106 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/categories/categories.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/categories/categories_bloc.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/simple_bloc_delegatae.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/category/category_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/loading_screen.dart'; @@ -39,7 +39,7 @@ void main() async { providers: [ BlocProvider( create: (context) { - return AuthenticationBloc()..add(AppStarted()); + return AuthenticationBloc()..add(const AppStarted()); }, ), BlocProvider( @@ -106,27 +106,26 @@ class _AppState extends State { ), home: BlocBuilder( builder: (context, state) { - if (state is AuthenticationUninitialized) { - return const SplashPage(); - } else if (state is AuthenticationAuthenticated) { - IntentRepository().handleIntent(); - if (BlocProvider.of(context).state - is CategoriesInitial) { - BlocProvider.of(context) - .add(CategoriesLoaded()); - } - return const CategoryScreen(); - } else if (state is AuthenticationUnauthenticated) { - return const LoginScreen(); - } else if (state is AuthenticationInvalid) { - return const LoginScreen( - invalidCredentials: true, - ); - } else if (state is AuthenticationLoading || - state is AuthenticationError) { - return const LoadingScreen(); - } else { - return const LoadingScreen(); + switch (state.status) { + case AuthenticationStatus.uninitialized: + return const SplashPage(); + case AuthenticationStatus.authenticated: + IntentRepository().handleIntent(); + if (BlocProvider.of(context).state.status == + CategoriesStatus.initial) { + BlocProvider.of(context) + .add(const CategoriesLoaded()); + } + return const CategoryScreen(); + case AuthenticationStatus.unauthenticated: + return const LoginScreen(); + case AuthenticationStatus.invalid: + return const LoginScreen( + invalidCredentials: true, + ); + case AuthenticationStatus.loading: + case AuthenticationStatus.error: + return const LoadingScreen(); } }, ), diff --git a/lib/src/blocs/authentication/authentication.dart b/lib/src/blocs/authentication/authentication.dart deleted file mode 100644 index 69b98588..00000000 --- a/lib/src/blocs/authentication/authentication.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'authentication_bloc.dart'; -export 'authentication_event.dart'; -export 'authentication_state.dart'; diff --git a/lib/src/blocs/authentication/authentication_bloc.dart b/lib/src/blocs/authentication/authentication_bloc.dart index 4a4acad2..78753a4d 100644 --- a/lib/src/blocs/authentication/authentication_bloc.dart +++ b/lib/src/blocs/authentication/authentication_bloc.dart @@ -1,56 +1,70 @@ -import 'dart:async'; - +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +part 'authentication_event.dart'; +part 'authentication_state.dart'; + class AuthenticationBloc extends Bloc { final UserRepository userRepository = UserRepository(); - AuthenticationBloc() : super(AuthenticationUninitialized()); + AuthenticationBloc() : super(AuthenticationState()) { + on(_mapAppStartedEventToState); + on(_mapLoggedInEventToState); + on(_mapLoggedOutEventToState); + } - @override - Stream mapEventToState( - AuthenticationEvent event, - ) async* { - if (event is AppStarted) { - final bool hasToken = await userRepository.hasAppAuthentication(); + Future _mapAppStartedEventToState( + AppStarted event, + Emitter emit, + ) async { + final bool hasToken = await userRepository.hasAppAuthentication(); - if (hasToken) { - yield AuthenticationLoading(); - await userRepository.loadAppAuthentication(); - bool validCredentials = false; - try { - validCredentials = await userRepository.checkAppAuthentication(); - } catch (e) { - yield AuthenticationError(e.toString()); - return; - } - if (validCredentials) { - await userRepository.fetchApiVersion(); - yield AuthenticationAuthenticated(); - } else { - await userRepository.deleteAppAuthentication(); - yield AuthenticationInvalid(); - } + if (hasToken) { + emit(AuthenticationState(status: AuthenticationStatus.loading)); + await userRepository.loadAppAuthentication(); + bool validCredentials = false; + try { + validCredentials = await userRepository.checkAppAuthentication(); + } catch (e) { + emit( + AuthenticationState( + status: AuthenticationStatus.error, + error: e.toString(), + ), + ); + return; + } + if (validCredentials) { + await userRepository.fetchApiVersion(); + emit(AuthenticationState(status: AuthenticationStatus.authenticated)); } else { - yield AuthenticationUnauthenticated(); + await userRepository.deleteAppAuthentication(); + emit(AuthenticationState(status: AuthenticationStatus.invalid)); } + } else { + emit(AuthenticationState(status: AuthenticationStatus.unauthenticated)); } + } - if (event is LoggedIn) { - yield AuthenticationLoading(); - await userRepository.persistAppAuthentication(event.appAuthentication); - await userRepository.fetchApiVersion(); - yield AuthenticationAuthenticated(); - } + Future _mapLoggedInEventToState( + LoggedIn event, + Emitter emit, + ) async { + emit(AuthenticationState(status: AuthenticationStatus.loading)); + await userRepository.persistAppAuthentication(event.appAuthentication); + await userRepository.fetchApiVersion(); + emit(AuthenticationState(status: AuthenticationStatus.authenticated)); + } - if (event is LoggedOut) { - yield AuthenticationLoading(); - await userRepository.deleteAppAuthentication(); - yield AuthenticationUnauthenticated(); - } + Future _mapLoggedOutEventToState( + LoggedOut event, + Emitter emit, + ) async { + emit(AuthenticationState(status: AuthenticationStatus.loading)); + await userRepository.deleteAppAuthentication(); + emit(AuthenticationState(status: AuthenticationStatus.unauthenticated)); } } diff --git a/lib/src/blocs/authentication/authentication_event.dart b/lib/src/blocs/authentication/authentication_event.dart index 81177a0f..47634c4d 100644 --- a/lib/src/blocs/authentication/authentication_event.dart +++ b/lib/src/blocs/authentication/authentication_event.dart @@ -1,5 +1,4 @@ -import 'package:equatable/equatable.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; +part of 'authentication_bloc.dart'; abstract class AuthenticationEvent extends Equatable { const AuthenticationEvent(); @@ -8,7 +7,9 @@ abstract class AuthenticationEvent extends Equatable { List get props => []; } -class AppStarted extends AuthenticationEvent {} +class AppStarted extends AuthenticationEvent { + const AppStarted(); +} class LoggedIn extends AuthenticationEvent { final AppAuthentication appAuthentication; @@ -22,4 +23,6 @@ class LoggedIn extends AuthenticationEvent { String toString() => appAuthentication.toString(); } -class LoggedOut extends AuthenticationEvent {} +class LoggedOut extends AuthenticationEvent { + const LoggedOut(); +} diff --git a/lib/src/blocs/authentication/authentication_state.dart b/lib/src/blocs/authentication/authentication_state.dart index c6c8e151..1ab56827 100644 --- a/lib/src/blocs/authentication/authentication_state.dart +++ b/lib/src/blocs/authentication/authentication_state.dart @@ -1,27 +1,26 @@ -import 'package:equatable/equatable.dart'; - -abstract class AuthenticationState extends Equatable { - const AuthenticationState(); - - @override - List get props => []; +part of 'authentication_bloc.dart'; + +enum AuthenticationStatus { + unauthenticated, + uninitialized, + authenticated, + invalid, + loading, + error; } -class AuthenticationUninitialized extends AuthenticationState {} - -class AuthenticationAuthenticated extends AuthenticationState {} +class AuthenticationState extends Equatable { + final AuthenticationStatus status; + final String? error; -class AuthenticationUnauthenticated extends AuthenticationState {} - -class AuthenticationInvalid extends AuthenticationState {} - -class AuthenticationError extends AuthenticationState { - final String errorMsg; - - const AuthenticationError(this.errorMsg); + const AuthenticationState({ + this.status = AuthenticationStatus.uninitialized, + this.error, + }) : assert( + (status != AuthenticationStatus.error && error == null) || + (status == AuthenticationStatus.error && error != null), + ); @override - List get props => [errorMsg]; + List get props => [status, error]; } - -class AuthenticationLoading extends AuthenticationState {} diff --git a/lib/src/blocs/categories/categories.dart b/lib/src/blocs/categories/categories.dart deleted file mode 100644 index d631a43a..00000000 --- a/lib/src/blocs/categories/categories.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'categories_bloc.dart'; -export 'categories_event.dart'; -export 'categories_state.dart'; diff --git a/lib/src/blocs/categories/categories_bloc.dart b/lib/src/blocs/categories/categories_bloc.dart index 4b83884d..6e2bea50 100644 --- a/lib/src/blocs/categories/categories_bloc.dart +++ b/lib/src/blocs/categories/categories_bloc.dart @@ -1,32 +1,47 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:nextcloud_cookbook_flutter/src/blocs/categories/categories.dart'; import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; +part 'categories_event.dart'; +part 'categories_state.dart'; + class CategoriesBloc extends Bloc { final DataRepository dataRepository = DataRepository(); - CategoriesBloc() : super(CategoriesInitial()); - - @override - Stream mapEventToState(CategoriesEvent event) async* { - if (event is CategoriesLoaded) { - yield* _mapCategoriesLoadedToState(); - } + CategoriesBloc() : super(CategoriesState()) { + on(_mapCategoriesLoadedEventToState); } - Stream _mapCategoriesLoadedToState() async* { + Future _mapCategoriesLoadedEventToState( + CategoriesLoaded event, + Emitter emit, + ) async { try { - yield CategoriesLoadInProgress(); + emit(CategoriesState(status: CategoriesStatus.loadInProgress)); final List categories = await dataRepository.fetchCategories(); dataRepository.updateCategoryNames(categories); - yield CategoriesLoadSuccess(categories: categories); + emit( + CategoriesState( + status: CategoriesStatus.loadSuccess, + categories: categories, + ), + ); final List categoriesWithImage = await dataRepository.fetchCategoryMainRecipes(categories); - yield CategoriesImageLoadSuccess(categories: categoriesWithImage); + emit( + CategoriesState( + status: CategoriesStatus.imageLoadSuccess, + categories: categoriesWithImage, + ), + ); } on Exception catch (e) { - yield CategoriesLoadFailure(e.toString()); + emit( + CategoriesState( + status: CategoriesStatus.loadFailure, + error: e.toString(), + ), + ); } } } diff --git a/lib/src/blocs/categories/categories_event.dart b/lib/src/blocs/categories/categories_event.dart index fe5cf848..e12ac9d7 100644 --- a/lib/src/blocs/categories/categories_event.dart +++ b/lib/src/blocs/categories/categories_event.dart @@ -1,4 +1,4 @@ -import 'package:equatable/equatable.dart'; +part of 'categories_bloc.dart'; abstract class CategoriesEvent extends Equatable { const CategoriesEvent(); @@ -7,4 +7,6 @@ abstract class CategoriesEvent extends Equatable { List get props => []; } -class CategoriesLoaded extends CategoriesEvent {} +class CategoriesLoaded extends CategoriesEvent { + const CategoriesLoaded(); +} diff --git a/lib/src/blocs/categories/categories_state.dart b/lib/src/blocs/categories/categories_state.dart index 7fbcbabc..c0c0b252 100644 --- a/lib/src/blocs/categories/categories_state.dart +++ b/lib/src/blocs/categories/categories_state.dart @@ -1,40 +1,37 @@ -import 'package:equatable/equatable.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; - -abstract class CategoriesState extends Equatable { - const CategoriesState(); - - @override - List get props => []; -} - -class CategoriesInitial extends CategoriesState {} - -class CategoriesLoadSuccess extends CategoriesState { - final List categories; - - const CategoriesLoadSuccess({required this.categories}); - - @override - List get props => [categories]; -} - -class CategoriesImageLoadSuccess extends CategoriesState { - final List categories; - - const CategoriesImageLoadSuccess({required this.categories}); - - @override - List get props => [categories]; +part of 'categories_bloc.dart'; + +enum CategoriesStatus { + initial, + loadInProgress, + loadFailure, + loadSuccess, + imageLoadSuccess; } -class CategoriesLoadFailure extends CategoriesState { - final String errorMsg; - - const CategoriesLoadFailure(this.errorMsg); +class CategoriesState extends Equatable { + final CategoriesStatus status; + final String? error; + final Iterable? categories; + + CategoriesState({ + this.status = CategoriesStatus.initial, + this.error, + this.categories, + }) { + switch (status) { + case CategoriesStatus.initial: + case CategoriesStatus.loadInProgress: + assert(error == null && categories == null); + break; + case CategoriesStatus.loadSuccess: + case CategoriesStatus.imageLoadSuccess: + assert(error == null && categories != null); + break; + case CategoriesStatus.loadFailure: + assert(error != null && categories == null); + } + } @override - List get props => [errorMsg]; + List get props => [status, error, categories]; } - -class CategoriesLoadInProgress extends CategoriesState {} diff --git a/lib/src/blocs/login/login.dart b/lib/src/blocs/login/login.dart deleted file mode 100644 index 7aff76e4..00000000 --- a/lib/src/blocs/login/login.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'login_bloc.dart'; -export 'login_event.dart'; -export 'login_state.dart'; diff --git a/lib/src/blocs/login/login_bloc.dart b/lib/src/blocs/login/login_bloc.dart index 743fdff5..3901ba57 100644 --- a/lib/src/blocs/login/login_bloc.dart +++ b/lib/src/blocs/login/login_bloc.dart @@ -1,49 +1,56 @@ -import 'dart:async'; - +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/login/login.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +part 'login_event.dart'; +part 'login_state.dart'; + class LoginBloc extends Bloc { final UserRepository userRepository = UserRepository(); final AuthenticationBloc authenticationBloc; LoginBloc({ required this.authenticationBloc, - }) : super(LoginInitial()); - - @override - Stream mapEventToState(LoginEvent event) async* { - if (event is LoginButtonPressed) { - yield LoginLoading(); - - try { - AppAuthentication appAuthentication; - - if (event.isAppPassword) { - appAuthentication = await userRepository.authenticateAppPassword( - event.serverURL, - event.username, - event.originalBasicAuth, - isSelfSignedCertificate: event.isSelfSignedCertificate, - ); - } else { - appAuthentication = await userRepository.authenticate( - event.serverURL, - event.username, - event.originalBasicAuth, - isSelfSignedCertificate: event.isSelfSignedCertificate, - ); - } + }) : super(LoginState()) { + on(_mapLoginButtonPressedEventToState); + } - authenticationBloc.add(LoggedIn(appAuthentication: appAuthentication)); - yield LoginInitial(); - } catch (error) { - yield LoginFailure(error: error.toString()); + Future _mapLoginButtonPressedEventToState( + LoginButtonPressed event, + Emitter emit, + ) async { + emit(LoginState(status: LoginStatus.loading)); + + try { + AppAuthentication appAuthentication; + + if (!event.isAppPassword) { + appAuthentication = await userRepository.authenticate( + event.serverURL, + event.username, + event.originalBasicAuth, + isSelfSignedCertificate: event.isSelfSignedCertificate, + ); + } else { + appAuthentication = await userRepository.authenticateAppPassword( + event.serverURL, + event.username, + event.originalBasicAuth, + isSelfSignedCertificate: event.isSelfSignedCertificate, + ); } + + authenticationBloc.add(LoggedIn(appAuthentication: appAuthentication)); + emit(LoginState()); + } catch (error) { + emit( + LoginState( + status: LoginStatus.failure, + error: error.toString(), + ), + ); } } } diff --git a/lib/src/blocs/login/login_event.dart b/lib/src/blocs/login/login_event.dart index 06ec8f96..4f8fa4dc 100644 --- a/lib/src/blocs/login/login_event.dart +++ b/lib/src/blocs/login/login_event.dart @@ -1,4 +1,4 @@ -import 'package:equatable/equatable.dart'; +part of 'login_bloc.dart'; abstract class LoginEvent extends Equatable { const LoginEvent(); diff --git a/lib/src/blocs/login/login_state.dart b/lib/src/blocs/login/login_state.dart index d7800b4c..535026c8 100644 --- a/lib/src/blocs/login/login_state.dart +++ b/lib/src/blocs/login/login_state.dart @@ -1,24 +1,28 @@ -import 'package:equatable/equatable.dart'; +part of 'login_bloc.dart'; -abstract class LoginState extends Equatable { - const LoginState(); - - @override - List get props => []; +enum LoginStatus { + initial, + loading, + failure; } -class LoginInitial extends LoginState {} - -class LoginLoading extends LoginState {} - -class LoginFailure extends LoginState { - final String error; +class LoginState extends Equatable { + final LoginStatus status; + final String? error; - const LoginFailure({required this.error}); + const LoginState({ + this.status = LoginStatus.initial, + this.error, + }) : assert( + (status != LoginStatus.failure && error == null) || + (status == LoginStatus.failure && error != null), + ); @override - List get props => [error]; + List get props => [status, error]; @override - String toString() => 'LoginFailure { error: $error }'; + String toString() => status == LoginStatus.failure + ? 'LoginFailure { error: $error }' + : 'Instance of LoginState'; } diff --git a/lib/src/blocs/recipe/recipe.dart b/lib/src/blocs/recipe/recipe.dart deleted file mode 100644 index d2dc0754..00000000 --- a/lib/src/blocs/recipe/recipe.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'recipe_bloc.dart'; -export 'recipe_event.dart'; -export 'recipe_state.dart'; diff --git a/lib/src/blocs/recipe/recipe_bloc.dart b/lib/src/blocs/recipe/recipe_bloc.dart index 5ed4414e..44b1468a 100644 --- a/lib/src/blocs/recipe/recipe_bloc.dart +++ b/lib/src/blocs/recipe/recipe_bloc.dart @@ -1,74 +1,85 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; +part 'recipe_event.dart'; +part 'recipe_state.dart'; + class RecipeBloc extends Bloc { final DataRepository dataRepository = DataRepository(); - RecipeBloc() : super(RecipeInitial()); - - @override - Stream mapEventToState(RecipeEvent event) async* { - if (event is RecipeLoaded) { - yield* _mapRecipeLoadedToState(event); - } else if (event is RecipeUpdated) { - yield* _mapRecipeUpdatedToState(event); - } else if (event is RecipeImported) { - yield* _mapRecipeImportedToState(event); - } else if (event is RecipeCreated) { - yield* _mapRecipeCreatedToState(event); - } + RecipeBloc() : super(RecipeState()) { + on(_mapRecipeLoadedToState); + on(_mapRecipeUpdatedToState); + on(_mapRecipeImportedToState); + on(_mapRecipeCreatedToState); } - Stream _mapRecipeLoadedToState( + Future _mapRecipeLoadedToState( RecipeLoaded recipeLoaded, - ) async* { + Emitter emit, + ) async { try { - yield RecipeLoadInProgress(); + emit(RecipeState(status: RecipeStatus.loadInProgress)); final recipe = await dataRepository.fetchRecipe(recipeLoaded.recipeId); - yield RecipeLoadSuccess(recipe); - } catch (_) { - yield RecipeLoadFailure(_.toString()); + emit(RecipeState(status: RecipeStatus.loadSuccess, recipe: recipe)); + } catch (e) { + emit(RecipeState(status: RecipeStatus.loadFailure, error: e.toString())); } } - Stream _mapRecipeUpdatedToState( + Future _mapRecipeUpdatedToState( RecipeUpdated recipeUpdated, - ) async* { + Emitter emit, + ) async { try { - yield RecipeUpdateInProgress(); - final String recipeId = - await dataRepository.updateRecipe(recipeUpdated.recipe); - yield RecipeUpdateSuccess(recipeId); - } catch (_) { - yield RecipeUpdateFailure(_.toString()); + emit(RecipeState(status: RecipeStatus.updateInProgress)); + final recipeId = await dataRepository.updateRecipe(recipeUpdated.recipe); + emit(RecipeState(status: RecipeStatus.updateSuccess, recipeId: recipeId)); + } catch (e) { + emit( + RecipeState( + status: RecipeStatus.updateFailure, + error: e.toString(), + ), + ); } } - Stream _mapRecipeCreatedToState( + Future _mapRecipeCreatedToState( RecipeCreated recipeCreated, - ) async* { + Emitter emit, + ) async { try { - yield RecipeCreateInProgress(); - final String recipeId = - await dataRepository.createRecipe(recipeCreated.recipe); - yield RecipeCreateSuccess(recipeId); - } catch (_) { - yield RecipeCreateFailure(_.toString()); + emit(RecipeState(status: RecipeStatus.createInProgress)); + final recipeId = await dataRepository.createRecipe(recipeCreated.recipe); + emit(RecipeState(status: RecipeStatus.createSuccess, recipeId: recipeId)); + } catch (e) { + emit( + RecipeState( + status: RecipeStatus.createFailure, + error: e.toString(), + ), + ); } } - Stream _mapRecipeImportedToState( + Future _mapRecipeImportedToState( RecipeImported recipeImported, - ) async* { + Emitter emit, + ) async { try { - yield RecipeImportInProgress(); - final Recipe recipe = - await dataRepository.importRecipe(recipeImported.url); - yield RecipeImportSuccess(recipe.id); - } catch (_) { - yield RecipeImportFailure(_.toString()); + emit(RecipeState(status: RecipeStatus.importInProgress)); + final recipe = await dataRepository.importRecipe(recipeImported.url); + emit(RecipeState(status: RecipeStatus.importSuccess, recipe: recipe)); + } catch (e) { + emit( + RecipeState( + status: RecipeStatus.importFailure, + error: e.toString(), + ), + ); } } } diff --git a/lib/src/blocs/recipe/recipe_event.dart b/lib/src/blocs/recipe/recipe_event.dart index 050e36e4..a6ae5189 100644 --- a/lib/src/blocs/recipe/recipe_event.dart +++ b/lib/src/blocs/recipe/recipe_event.dart @@ -1,11 +1,10 @@ -import 'package:equatable/equatable.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; +part of 'recipe_bloc.dart'; abstract class RecipeEvent extends Equatable { const RecipeEvent(); @override - List get props => []; + List get props => []; } class RecipeLoaded extends RecipeEvent { @@ -23,7 +22,7 @@ class RecipeUpdated extends RecipeEvent { const RecipeUpdated(this.recipe); @override - List get props => [recipe]; + List get props => [recipe]; } class RecipeCreated extends RecipeEvent { @@ -32,7 +31,7 @@ class RecipeCreated extends RecipeEvent { const RecipeCreated(this.recipe); @override - List get props => [recipe]; + List get props => [recipe]; } class RecipeImported extends RecipeEvent { @@ -41,5 +40,5 @@ class RecipeImported extends RecipeEvent { const RecipeImported(this.url); @override - List get props => [url]; + List get props => [url]; } diff --git a/lib/src/blocs/recipe/recipe_state.dart b/lib/src/blocs/recipe/recipe_state.dart index ad987d4b..a0ceac83 100644 --- a/lib/src/blocs/recipe/recipe_state.dart +++ b/lib/src/blocs/recipe/recipe_state.dart @@ -1,84 +1,62 @@ -import 'package:equatable/equatable.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; - -abstract class RecipeState extends Equatable { - const RecipeState(); +part of 'recipe_bloc.dart'; + +enum RecipeStatus { + initial, + failure, + success, + loadSuccess, + loadFailure, + loadInProgress, + updateFailure, + updateInProgress, + updateSuccess, + createFailure, + createSuccess, + createInProgress, + importSuccess, + importFailure, + importInProgress; +} + +class RecipeState extends Equatable { + final RecipeStatus status; + final String? error; + final Recipe? recipe; + final String? recipeId; + + RecipeState({ + this.status = RecipeStatus.initial, + this.error, + this.recipe, + this.recipeId, + }) { + switch (status) { + case RecipeStatus.initial: + + case RecipeStatus.loadInProgress: + case RecipeStatus.updateInProgress: + case RecipeStatus.createInProgress: + case RecipeStatus.importInProgress: + assert(error == null && recipe == null && recipeId == null); + break; + case RecipeStatus.createSuccess: + case RecipeStatus.updateSuccess: + assert(error == null && recipe == null && recipeId != null); + break; + case RecipeStatus.success: + case RecipeStatus.loadSuccess: + case RecipeStatus.importSuccess: + assert(error == null && recipe != null && recipeId == null); + break; + case RecipeStatus.failure: + case RecipeStatus.loadFailure: + case RecipeStatus.updateFailure: + case RecipeStatus.createFailure: + case RecipeStatus.importFailure: + assert(error != null && recipe == null && recipeId == null); + } + } @override - List get props => []; + List get props => [status, error, recipe, recipeId]; } - -class RecipeInitial extends RecipeState {} - -class RecipeFailure extends RecipeState { - final String errorMsg; - - const RecipeFailure(this.errorMsg); - - @override - List get props => [errorMsg]; -} - -class RecipeSuccess extends RecipeState { - final Recipe recipe; - - const RecipeSuccess(this.recipe); - - @override - List get props => [recipe]; -} - -class RecipeLoadSuccess extends RecipeSuccess { - const RecipeLoadSuccess(super.recipe); -} - -class RecipeLoadFailure extends RecipeFailure { - const RecipeLoadFailure(super.errorMsg); -} - -class RecipeLoadInProgress extends RecipeState {} - -class RecipeUpdateFailure extends RecipeFailure { - const RecipeUpdateFailure(super.errorMsg); -} - -class RecipeUpdateSuccess extends RecipeState { - final String recipeId; - - const RecipeUpdateSuccess(this.recipeId); - - @override - List get props => [recipeId]; -} - -class RecipeUpdateInProgress extends RecipeState {} - -class RecipeCreateFailure extends RecipeFailure { - const RecipeCreateFailure(super.errorMsg); -} - -class RecipeCreateSuccess extends RecipeState { - final String recipeId; - - const RecipeCreateSuccess(this.recipeId); - - @override - List get props => [recipeId]; -} - -class RecipeCreateInProgress extends RecipeState {} - -class RecipeImportSuccess extends RecipeState { - final String recipeId; - - const RecipeImportSuccess(this.recipeId); - - @override - List get props => [recipeId]; -} - -class RecipeImportFailure extends RecipeFailure { - const RecipeImportFailure(super.errorMsg); -} - -class RecipeImportInProgress extends RecipeState {} diff --git a/lib/src/blocs/recipes_short/recipes_short.dart b/lib/src/blocs/recipes_short/recipes_short.dart deleted file mode 100644 index 496cae75..00000000 --- a/lib/src/blocs/recipes_short/recipes_short.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'recipes_short_bloc.dart'; -export 'recipes_short_event.dart'; -export 'recipes_short_state.dart'; diff --git a/lib/src/blocs/recipes_short/recipes_short_bloc.dart b/lib/src/blocs/recipes_short/recipes_short_bloc.dart index c32a4785..9a8b0149 100644 --- a/lib/src/blocs/recipes_short/recipes_short_bloc.dart +++ b/lib/src/blocs/recipes_short/recipes_short_bloc.dart @@ -1,44 +1,67 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_event.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_state.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; +part 'recipes_short_event.dart'; +part 'recipes_short_state.dart'; + class RecipesShortBloc extends Bloc { final DataRepository dataRepository = DataRepository(); - RecipesShortBloc() : super(RecipesShortLoadInProgress()); - - @override - Stream mapEventToState(RecipesShortEvent event) async* { - if (event is RecipesShortLoaded) { - yield* _mapRecipesShortLoadedToState(event); - } else if (event is RecipesShortLoadedAll) { - yield* _mapRecipesShortLoadedAllToState(event); - } + RecipesShortBloc() : super(RecipesShortState()) { + on(_mapRecipesShortLoadedToState); + on(_mapRecipesShortLoadedAllToState); } - Stream _mapRecipesShortLoadedToState( + Future _mapRecipesShortLoadedToState( RecipesShortLoaded recipesShortLoaded, - ) async* { + Emitter emit, + ) async { try { final recipesShort = await dataRepository.fetchRecipesShort( category: recipesShortLoaded.category, ); - yield RecipesShortLoadSuccess(recipesShort); + emit( + RecipesShortState( + status: RecipesShortStatus.loadSuccess, + recipesShort: recipesShort, + ), + ); } catch (_) { - yield RecipesShortLoadFailure(); + emit( + RecipesShortState( + status: RecipesShortStatus.loadFailure, + error: "", + ), + ); } } - Stream _mapRecipesShortLoadedAllToState( + Future _mapRecipesShortLoadedAllToState( RecipesShortLoadedAll recipesShortLoadedAll, - ) async* { + Emitter emit, + ) async { try { - yield RecipesShortLoadAllInProgress(); + emit( + RecipesShortState( + status: RecipesShortStatus.loadAllInProgress, + ), + ); final recipesShort = await dataRepository.fetchAllRecipes(); - yield RecipesShortLoadAllSuccess(recipesShort); + emit( + RecipesShortState( + status: RecipesShortStatus.loadAllSuccess, + recipesShort: recipesShort, + ), + ); } catch (e) { - yield RecipesShortLoadAllFailure(e.toString()); + emit( + RecipesShortState( + status: RecipesShortStatus.loadAllFailure, + error: "", + ), + ); } } } diff --git a/lib/src/blocs/recipes_short/recipes_short_event.dart b/lib/src/blocs/recipes_short/recipes_short_event.dart index cbf4a8e7..c7a35f61 100644 --- a/lib/src/blocs/recipes_short/recipes_short_event.dart +++ b/lib/src/blocs/recipes_short/recipes_short_event.dart @@ -1,4 +1,4 @@ -import 'package:equatable/equatable.dart'; +part of 'recipes_short_bloc.dart'; abstract class RecipesShortEvent extends Equatable { const RecipesShortEvent(); diff --git a/lib/src/blocs/recipes_short/recipes_short_state.dart b/lib/src/blocs/recipes_short/recipes_short_state.dart index 60889b00..bfc6f308 100644 --- a/lib/src/blocs/recipes_short/recipes_short_state.dart +++ b/lib/src/blocs/recipes_short/recipes_short_state.dart @@ -1,48 +1,40 @@ -import 'package:equatable/equatable.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; - -abstract class RecipesShortState extends Equatable { - const RecipesShortState(); - - @override - List get props => []; +part of 'recipes_short_bloc.dart'; + +enum RecipesShortStatus { + loadInProgress, + loadFailure, + loadSuccess, + loadAllSuccess, + loadAllFailure, + loadAllInProgress; } -class RecipesShortLoadInProgress extends RecipesShortState {} - -class RecipesShortLoadFailure extends RecipesShortState {} - -class RecipesShortLoadSuccess extends RecipesShortState { - final List recipesShort; - - const RecipesShortLoadSuccess(this.recipesShort); +class RecipesShortState extends Equatable { + final RecipesShortStatus status; + final String? error; + final Iterable? recipesShort; + + RecipesShortState({ + this.status = RecipesShortStatus.loadInProgress, + this.error, + this.recipesShort, + }) { + switch (status) { + case RecipesShortStatus.loadInProgress: + case RecipesShortStatus.loadAllInProgress: + assert(error == null, recipesShort == null); + break; + case RecipesShortStatus.loadAllSuccess: + case RecipesShortStatus.loadSuccess: + assert(error == null, recipesShort != null); + break; + case RecipesShortStatus.loadAllFailure: + case RecipesShortStatus.loadFailure: + assert(error != null, recipesShort == null); + break; + } + } @override - List get props => recipesShort; - - @override - String toString() => 'RecipesShortLoadSuccess { recipes: $recipesShort }'; + List get props => [status, error, recipesShort]; } - -class RecipesShortLoadAllSuccess extends RecipesShortState { - final List recipesShort; - - const RecipesShortLoadAllSuccess(this.recipesShort); - - @override - List get props => recipesShort; - - @override - String toString() => 'RecipesShortLoadAllSuccess { recipes: $recipesShort }'; -} - -class RecipesShortLoadAllFailure extends RecipesShortState { - final String errorMsg; - - const RecipesShortLoadAllFailure(this.errorMsg); - - @override - List get props => [errorMsg]; -} - -class RecipesShortLoadAllInProgress extends RecipesShortState {} diff --git a/lib/src/screens/category/category_screen.dart b/lib/src/screens/category/category_screen.dart index a93495c5..6cd914cc 100644 --- a/lib/src/screens/category/category_screen.dart +++ b/lib/src/screens/category/category_screen.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/categories/categories.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/categories/categories_bloc.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; @@ -128,7 +128,7 @@ class _CategoryScreenState extends State { title: Text(translate('app_bar.logout')), onTap: () { BlocProvider.of(context) - .add(LoggedOut()); + .add(const LoggedOut()); }, ), ], @@ -138,14 +138,14 @@ class _CategoryScreenState extends State { title: Text(translate('categories.title')), actions: [ BlocBuilder( - builder: (context, recipeShortState) { + builder: (context, state) { return BlocListener( - listener: (context, recipeShortState) { - if (recipeShortState is RecipesShortLoadAllSuccess) { + listener: (context, state) { + if (state.status == RecipesShortStatus.loadAllSuccess) { showSearch( context: context, delegate: SearchPage( - items: recipeShortState.recipesShort, + items: state.recipesShort!.toList(), searchLabel: translate('search.title'), suggestion: const Center( // child: Text('Filter people by name, surname or age'), @@ -173,14 +173,14 @@ class _CategoryScreenState extends State { ), ), ); - } else if (recipeShortState - is RecipesShortLoadAllFailure) { + } else if (state.status == + RecipesShortStatus.loadAllFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( translate( 'search.errors.search_failed', - args: {"error_msg": recipeShortState.errorMsg}, + args: {"error_msg": state.error}, ), ), backgroundColor: Colors.red, @@ -191,14 +191,13 @@ class _CategoryScreenState extends State { child: IconButton( icon: Icon( () { - if (recipeShortState - is RecipesShortLoadAllInProgress) { - return Icons.downloading; - } else if (recipeShortState - is RecipesShortLoadAllFailure) { - return Icons.report_problem; - } else { - return Icons.search; + switch (state.status) { + case RecipesShortStatus.loadAllInProgress: + return Icons.downloading; + case RecipesShortStatus.loadAllFailure: + return Icons.report_problem; + default: + return Icons.search; } }(), semanticLabel: translate('app_bar.search'), @@ -219,7 +218,7 @@ class _CategoryScreenState extends State { onPressed: () { DefaultCacheManager().emptyCache(); BlocProvider.of(context) - .add(CategoriesLoaded()); + .add(const CategoriesLoaded()); }, ), ], @@ -227,48 +226,50 @@ class _CategoryScreenState extends State { body: RefreshIndicator( onRefresh: () { DefaultCacheManager().emptyCache(); - BlocProvider.of(context).add(CategoriesLoaded()); + BlocProvider.of(context) + .add(const CategoriesLoaded()); return Future.value(); }, child: () { - if (categoriesState is CategoriesLoadSuccess) { - return _buildCategoriesScreen(categoriesState.categories); - } else if (categoriesState is CategoriesImageLoadSuccess) { - return _buildCategoriesScreen(categoriesState.categories); - } else if (categoriesState is CategoriesLoadInProgress || - categoriesState is CategoriesInitial) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: SpinKitWave( - color: Theme.of(context).primaryColor, - ), - ), - const ApiVersionWarning(), - ], - ); - } else if (categoriesState is CategoriesLoadFailure) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( + switch (categoriesState.status) { + case CategoriesStatus.loadSuccess: + return _buildCategoriesScreen(categoriesState.categories!); + case CategoriesStatus.imageLoadSuccess: + return _buildCategoriesScreen(categoriesState.categories!); + case CategoriesStatus.loadInProgress: + case CategoriesStatus.initial: + return Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - translate('categories.errors.plugin_missing'), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const Divider(), - Text( - translate( - 'categories.errors.load_failed', - args: {'error_msg': categoriesState.errorMsg}, + Center( + child: SpinKitWave( + color: Theme.of(context).primaryColor, ), ), + const ApiVersionWarning(), ], - ), - ); - } else { - return Text(translate('categories.errors.unknown')); + ); + case CategoriesStatus.loadFailure: + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + translate('categories.errors.plugin_missing'), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const Divider(), + Text( + translate( + 'categories.errors.load_failed', + args: {'error_msg': categoriesState.error}, + ), + ), + ], + ), + ); + default: + return Text(translate('categories.errors.unknown')); } }(), ), @@ -277,7 +278,9 @@ class _CategoryScreenState extends State { ); } - Widget _buildCategoriesScreen(List categories) { + Widget _buildCategoriesScreen( + Iterable categories, + ) { final double screenWidth = MediaQuery.of(context).size.width; final int axisRatio = (screenWidth / 150).round(); final int axisCount = axisRatio < 1 ? 1 : axisRatio; diff --git a/lib/src/screens/form/login_form.dart b/lib/src/screens/form/login_form.dart index df2dc191..def61385 100644 --- a/lib/src/screens/form/login_form.dart +++ b/lib/src/screens/form/login_form.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/login/login.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/login/login_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/checkbox_form_field.dart'; import 'package:punycode/punycode.dart'; @@ -83,10 +83,10 @@ class _LoginFormState extends State with WidgetsBindingObserver { return BlocListener( listener: (context, state) { - if (state is LoginFailure) { + if (state.status == LoginStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(state.error), + content: Text(state.error!), backgroundColor: Colors.red, ), ); @@ -148,7 +148,7 @@ class _LoginFormState extends State with WidgetsBindingObserver { controller: _password, obscureText: true, onFieldSubmitted: (val) { - if (state is! LoginLoading) { + if (state.status != LoginStatus.loading) { onLoginButtonPressed(); } }, @@ -221,18 +221,15 @@ class _LoginFormState extends State with WidgetsBindingObserver { ), ), ElevatedButton( - onPressed: state is! LoginLoading + onPressed: state.status != LoginStatus.loading ? onLoginButtonPressed : null, child: Text(translate('login.button')), ), - Container( - child: state is LoginLoading - ? SpinKitWave( - color: Theme.of(context).primaryColor, - ) - : null, - ), + if (state.status == LoginStatus.loading) + SpinKitWave( + color: Theme.of(context).primaryColor, + ), ], ), ), diff --git a/lib/src/screens/form/recipe_form.dart b/lib/src/screens/form/recipe_form.dart index 9c9505da..e5956f04 100644 --- a/lib/src/screens/form/recipe_form.dart +++ b/lib/src/screens/form/recipe_form.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/input/duration_form_field.dart'; @@ -49,236 +49,241 @@ class _RecipeFormState extends State { @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, state) => Form( - key: _formKey, - child: Padding( - padding: const EdgeInsets.all(15.0), - child: SingleChildScrollView( - child: Wrap( - runSpacing: 20, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - translate('recipe.fields.name'), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + builder: (context, state) { + final enabled = state.status != RecipeStatus.updateInProgress; + return Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.all(15.0), + child: SingleChildScrollView( + child: Wrap( + runSpacing: 20, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate('recipe.fields.name'), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - TextFormField( - enabled: state is! RecipeUpdateInProgress, - initialValue: recipe.name, - onChanged: (value) { - _mutableRecipe.name = value; - }, - ), - ], - ), // Name - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - translate('recipe.fields.description'), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + TextFormField( + enabled: enabled, + initialValue: recipe.name, + onChanged: (value) { + _mutableRecipe.name = value; + }, ), - ), - TextFormField( - enabled: state is! RecipeUpdateInProgress, - initialValue: recipe.description, - maxLines: 100, - minLines: 1, - onChanged: (value) { - _mutableRecipe.description = value; - }, - ), - ], - ), // Description - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - translate('recipe.fields.category'), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + ], + ), // Name + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate('recipe.fields.description'), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - TypeAheadFormField( - getImmediateSuggestions: true, - textFieldConfiguration: TextFieldConfiguration( - controller: categoryController, + TextFormField( + enabled: enabled, + initialValue: recipe.description, + maxLines: 100, + minLines: 1, + onChanged: (value) { + _mutableRecipe.description = value; + }, ), - suggestionsCallback: - DataRepository().getMatchingCategoryNames, - itemBuilder: (context, String? suggestion) { - if (suggestion != null) { - return ListTile( - title: Text(suggestion), - ); - } - return const SizedBox(); - }, - onSuggestionSelected: (String? suggestion) { - if (suggestion == null) return; - categoryController.text = suggestion; - }, - onSaved: (value) { - if (value == null) return; - _mutableRecipe.recipeCategory = value; - }, - ) - ], - ), // Category - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - translate('recipe.fields.keywords'), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + ], + ), // Description + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate('recipe.fields.category'), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - TextFormField( - enabled: state is! RecipeUpdateInProgress, - initialValue: recipe.keywords, - onChanged: (value) { - _mutableRecipe.keywords = value; - }, - ), - ], - ), // Keywords - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - translate('recipe.fields.source'), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + TypeAheadFormField( + getImmediateSuggestions: true, + textFieldConfiguration: TextFieldConfiguration( + controller: categoryController, + ), + suggestionsCallback: + DataRepository().getMatchingCategoryNames, + itemBuilder: (context, String? suggestion) { + if (suggestion != null) { + return ListTile( + title: Text(suggestion), + ); + } + return const SizedBox(); + }, + onSuggestionSelected: (String? suggestion) { + if (suggestion == null) return; + categoryController.text = suggestion; + }, + onSaved: (value) { + if (value == null) return; + _mutableRecipe.recipeCategory = value; + }, + ) + ], + ), // Category + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate('recipe.fields.keywords'), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - TextFormField( - enabled: state is! RecipeUpdateInProgress, - initialValue: recipe.url, - onChanged: (value) { - _mutableRecipe.url = value; - }, - ), - ], - ), // URL - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - translate('recipe.fields.image'), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + TextFormField( + enabled: enabled, + initialValue: recipe.keywords, + onChanged: (value) { + _mutableRecipe.keywords = value; + }, ), - ), - TextFormField( - enabled: false, - style: const TextStyle(color: Colors.grey), - initialValue: recipe.imageUrl, - onChanged: (value) { - _mutableRecipe.imageUrl = value; - }, - ), - ], - ), // Image - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - translate('recipe.fields.servings'), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + ], + ), // Keywords + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate('recipe.fields.source'), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - IntegerTextFormField( - enabled: state is! RecipeUpdateInProgress, - initialValue: recipe.recipeYield, - onChanged: (value) => _mutableRecipe.recipeYield = value, - onSaved: (value) => _mutableRecipe.recipeYield = value, - ), - ], - ), // Servings - DurationFormField( - title: translate('recipe.fields.time.prep'), - state: state, - duration: recipe.prepTime, - onChanged: (value) => {_mutableRecipe.prepTime = value}, - ), - DurationFormField( - title: translate('recipe.fields.time.cook'), - state: state, - duration: recipe.cookTime, - onChanged: (value) => {_mutableRecipe.cookTime = value}, - ), - DurationFormField( - title: translate('recipe.fields.time.total'), - state: state, - duration: recipe.totalTime, - onChanged: (value) => {_mutableRecipe.totalTime = value}, - ), - ReorderableListFormField( - title: translate('recipe.fields.tools'), - items: recipe.tool, - state: state, - onSave: (value) => {_mutableRecipe.tool = value}, - ), - ReorderableListFormField( - title: translate('recipe.fields.ingredients'), - items: recipe.recipeIngredient, - state: state, - onSave: (value) => {_mutableRecipe.recipeIngredient = value}, - ), - ReorderableListFormField( - title: translate('recipe.fields.instructions'), - items: recipe.recipeInstructions, - state: state, - onSave: (value) => - {_mutableRecipe.recipeInstructions = value}, - ), - SizedBox( - width: 150, - child: ElevatedButton( - onPressed: () { - if (_formKey.currentState?.validate() ?? false) { - _formKey.currentState?.save(); - widget.recipeFormSubmit(_mutableRecipe, context); - } - }, - child: () { - switch (state.runtimeType) { - case RecipeUpdateInProgress: - return const SpinKitWave( - color: Colors.white, - size: 30.0, - ); - case RecipeUpdateFailure: - case RecipeUpdateSuccess: - case RecipeLoadSuccess: - case RecipeCreateSuccess: - case RecipeCreateFailure: - case RecipeInitial: - default: - return Text(widget.buttonSubmitText); - } - }(), + TextFormField( + enabled: enabled, + initialValue: recipe.url, + onChanged: (value) { + _mutableRecipe.url = value; + }, + ), + ], + ), // URL + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate('recipe.fields.image'), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + TextFormField( + enabled: false, + style: const TextStyle(color: Colors.grey), + initialValue: recipe.imageUrl, + onChanged: (value) { + _mutableRecipe.imageUrl = value; + }, + ), + ], + ), // Image + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate('recipe.fields.servings'), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + IntegerTextFormField( + enabled: enabled, + initialValue: recipe.recipeYield, + onChanged: (value) => + _mutableRecipe.recipeYield = value, + onSaved: (value) => _mutableRecipe.recipeYield = value, + ), + ], + ), // Servings + DurationFormField( + title: translate('recipe.fields.time.prep'), + state: state, + duration: recipe.prepTime, + onChanged: (value) => {_mutableRecipe.prepTime = value}, ), - ), // Update Button - ], + DurationFormField( + title: translate('recipe.fields.time.cook'), + state: state, + duration: recipe.cookTime, + onChanged: (value) => {_mutableRecipe.cookTime = value}, + ), + DurationFormField( + title: translate('recipe.fields.time.total'), + state: state, + duration: recipe.totalTime, + onChanged: (value) => {_mutableRecipe.totalTime = value}, + ), + ReorderableListFormField( + title: translate('recipe.fields.tools'), + items: recipe.tool, + state: state, + onSave: (value) => {_mutableRecipe.tool = value}, + ), + ReorderableListFormField( + title: translate('recipe.fields.ingredients'), + items: recipe.recipeIngredient, + state: state, + onSave: (value) => + {_mutableRecipe.recipeIngredient = value}, + ), + ReorderableListFormField( + title: translate('recipe.fields.instructions'), + items: recipe.recipeInstructions, + state: state, + onSave: (value) => + {_mutableRecipe.recipeInstructions = value}, + ), + SizedBox( + width: 150, + child: ElevatedButton( + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + _formKey.currentState?.save(); + widget.recipeFormSubmit(_mutableRecipe, context); + } + }, + child: () { + switch (state.status) { + case RecipeStatus.updateInProgress: + return const SpinKitWave( + color: Colors.white, + size: 30.0, + ); + case RecipeStatus.updateFailure: + case RecipeStatus.updateSuccess: + case RecipeStatus.loadSuccess: + case RecipeStatus.createSuccess: + case RecipeStatus.createFailure: + case RecipeStatus.initial: + default: + return Text(widget.buttonSubmitText); + } + }(), + ), + ), // Update Button + ], + ), ), ), - ), - ), + ); + }, ); } } diff --git a/lib/src/screens/form/recipe_import_form.dart b/lib/src/screens/form/recipe_import_form.dart index 785ea033..eed6f266 100644 --- a/lib/src/screens/form/recipe_import_form.dart +++ b/lib/src/screens/form/recipe_import_form.dart @@ -4,7 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; class RecipeImportForm extends StatefulWidget { final String importUrl; @@ -35,6 +35,7 @@ class _RecipeImportFormState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, RecipeState state) { + final enabled = state.status != RecipeStatus.updateInProgress; return SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(10.0), @@ -42,7 +43,7 @@ class _RecipeImportFormState extends State { child: Column( children: [ TextField( - enabled: state is! RecipeImportInProgress, + enabled: enabled, controller: _importUrlController, decoration: InputDecoration( hintText: translate("recipe_import.field"), @@ -60,32 +61,29 @@ class _RecipeImportFormState extends State { ), Center( child: TextButton( - onPressed: () => { - if (state is! RecipeImportInProgress) + onPressed: () { + if (enabled) { BlocProvider.of(context) - .add(RecipeImported(_importUrlController.text)) - else - null + .add(RecipeImported(_importUrlController.text)); + } }, - child: () { - return state is RecipeImportInProgress - ? SpinKitWave( - color: Theme.of(context).primaryColor, - size: 30.0, - ) - : Row( - children: [ - const Spacer(), - Padding( - padding: const EdgeInsets.only(right: 9.0), - child: - Text(translate("recipe_import.button")), - ), - const Icon(Icons.cloud_download_outlined), - const Spacer(), - ], - ); - }(), + child: enabled + ? Row( + children: [ + const Spacer(), + Padding( + padding: const EdgeInsets.only(right: 9.0), + child: + Text(translate("recipe_import.button")), + ), + const Icon(Icons.cloud_download_outlined), + const Spacer(), + ], + ) + : SpinKitWave( + color: Theme.of(context).primaryColor, + size: 30.0, + ), ), ) ], diff --git a/lib/src/screens/loading_screen.dart b/lib/src/screens/loading_screen.dart index 369bd72b..51d00cc4 100644 --- a/lib/src/screens/loading_screen.dart +++ b/lib/src/screens/loading_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; class LoadingScreen extends StatelessWidget { const LoadingScreen({super.key}); @@ -12,35 +12,34 @@ class LoadingScreen extends StatelessWidget { return Scaffold( body: BlocBuilder( builder: (context, authenticationState) { + if (authenticationState.status != AuthenticationStatus.error) { + return SpinKitWave( + color: Theme.of(context).primaryColor, + ); + } + return Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (authenticationState is! AuthenticationError) - SpinKitWave( - color: Theme.of(context).primaryColor, - ), - if (authenticationState is AuthenticationError) - Text(authenticationState.errorMsg), + Text(authenticationState.error!), const SizedBox(height: 10), - if (authenticationState is AuthenticationError) - ElevatedButton( - onPressed: () { - BlocProvider.of(context) - .add(AppStarted()); - }, - child: Text(translate("login.retry")), - ), + ElevatedButton( + onPressed: () { + BlocProvider.of(context) + .add(const AppStarted()); + }, + child: Text(translate("login.retry")), + ), const SizedBox(height: 10), - if (authenticationState is AuthenticationError) - ElevatedButton( - onPressed: () { - BlocProvider.of(context) - .add(LoggedOut()); - }, - child: Text(translate("login.reset")), - ), + ElevatedButton( + onPressed: () { + BlocProvider.of(context) + .add(const LoggedOut()); + }, + child: Text(translate("login.reset")), + ), ], ), ); diff --git a/lib/src/screens/recipe/recipe_screen.dart b/lib/src/screens/recipe/recipe_screen.dart index 69bc5594..73bef39d 100644 --- a/lib/src/screens/recipe/recipe_screen.dart +++ b/lib/src/screens/recipe/recipe_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe/widget/ingredient_list.dart'; @@ -75,7 +75,7 @@ class RecipeScreenState extends State { Icons.edit, ), onPressed: () async { - if (state is RecipeLoadSuccess) { + if (state.status == RecipeStatus.loadSuccess) { _disableWakelock(); await Navigator.push( context, @@ -83,7 +83,7 @@ class RecipeScreenState extends State { builder: (context) { return BlocProvider.value( value: recipeBloc, - child: RecipeEditScreen(state.recipe), + child: RecipeEditScreen(state.recipe!), ); }, ), @@ -94,24 +94,25 @@ class RecipeScreenState extends State { ), ], ), - floatingActionButton: state is RecipeLoadSuccess - ? _buildFabButton(state.recipe) + floatingActionButton: state.status == RecipeStatus.loadSuccess + ? _buildFabButton(state.recipe!) : null, body: () { - if (state is RecipeLoadSuccess) { - return _buildRecipeScreen(state.recipe); - } else if (state is RecipeLoadInProgress) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (state is RecipeFailure) { - return Center( - child: Text(state.errorMsg), - ); - } else { - return const Center( - child: Text("FAILED"), - ); + switch (state.status) { + case RecipeStatus.loadSuccess: + return _buildRecipeScreen(state.recipe!); + case RecipeStatus.loadInProgress: + return const Center( + child: CircularProgressIndicator(), + ); + case RecipeStatus.failure: + return Center( + child: Text(state.error!), + ); + default: + return const Center( + child: Text("FAILED"), + ); } }(), ), diff --git a/lib/src/screens/recipe_create_screen.dart b/lib/src/screens/recipe_create_screen.dart index 68141447..0d81d759 100644 --- a/lib/src/screens/recipe_create_screen.dart +++ b/lib/src/screens/recipe_create_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/form/recipe_form.dart'; @@ -23,24 +23,24 @@ class RecipeCreateScreen extends StatelessWidget { appBar: AppBar( title: BlocListener( listener: (BuildContext context, RecipeState state) { - if (state is RecipeCreateFailure) { + if (state.status == RecipeStatus.createFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( translate( 'recipe_create.errors.update_failed', - args: {"error_msg": state.errorMsg}, + args: {"error_msg": state.error}, ), ), backgroundColor: Colors.red, ), ); - } else if (state is RecipeCreateSuccess) { + } else if (state.status == RecipeStatus.createSuccess) { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => - RecipeScreen(recipeId: state.recipeId), + RecipeScreen(recipeId: state.recipeId!), ), ); } diff --git a/lib/src/screens/recipe_edit_screen.dart b/lib/src/screens/recipe_edit_screen.dart index 484222b2..f71ba639 100644 --- a/lib/src/screens/recipe_edit_screen.dart +++ b/lib/src/screens/recipe_edit_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/form/recipe_form.dart'; @@ -18,7 +18,7 @@ class RecipeEditScreen extends StatelessWidget { return WillPopScope( onWillPop: () { final RecipeBloc recipeBloc = BlocProvider.of(context); - if (recipeBloc.state is RecipeUpdateFailure) { + if (recipeBloc.state.status == RecipeStatus.updateFailure) { recipeBloc.add(RecipeLoaded(recipe.id)); } return Future(() => true); @@ -27,21 +27,21 @@ class RecipeEditScreen extends StatelessWidget { appBar: AppBar( title: BlocListener( listener: (BuildContext context, RecipeState state) { - if (state is RecipeUpdateFailure) { + if (state.status == RecipeStatus.updateFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( translate( 'recipe_edit.errors.update_failed', - args: {"error_msg": state.errorMsg}, + args: {"error_msg": state.error}, ), ), backgroundColor: Colors.red, ), ); - } else if (state is RecipeUpdateSuccess) { + } else if (state.status == RecipeStatus.updateSuccess) { BlocProvider.of(context) - .add(RecipeLoaded(state.recipeId)); + .add(RecipeLoaded(state.recipeId!)); Navigator.pop(context); } }, diff --git a/lib/src/screens/recipe_import_screen.dart b/lib/src/screens/recipe_import_screen.dart index 2a027f73..ef7f73d2 100644 --- a/lib/src/screens/recipe_import_screen.dart +++ b/lib/src/screens/recipe_import_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/form/recipe_import_form.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe/recipe_screen.dart'; @@ -19,24 +19,24 @@ class RecipeImportScreen extends StatelessWidget { title: BlocListener( child: Text(translate("recipe_import.title")), listener: (context, state) { - if (state is RecipeImportFailure) { + if (state.status == RecipeStatus.importFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( translate( 'recipe_import.errors.import_failed', - args: {"error_msg": state.errorMsg}, + args: {"error_msg": state.error}, ), ), backgroundColor: Colors.red, ), ); - } else if (state is RecipeImportSuccess) { + } else if (state.status == RecipeStatus.importSuccess) { Navigator.push( context, MaterialPageRoute( builder: (context) { - return RecipeScreen(recipeId: state.recipeId); + return RecipeScreen(recipeId: state.recipeId!); }, ), ); diff --git a/lib/src/screens/recipes_list_screen.dart b/lib/src/screens/recipes_list_screen.dart index edaf6556..6a39b239 100644 --- a/lib/src/screens/recipes_list_screen.dart +++ b/lib/src/screens/recipes_list_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe/recipe_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; @@ -63,8 +63,10 @@ class RecipesListScreenState extends State { return Future.value(); }, child: () { - if (recipesShortState is RecipesShortLoadSuccess) { - return _buildRecipesShortScreen(recipesShortState.recipesShort); + if (recipesShortState.status == RecipesShortStatus.loadSuccess) { + return _buildRecipesShortScreen( + recipesShortState.recipesShort!, + ); } else { return const Center(child: CircularProgressIndicator()); } @@ -75,13 +77,13 @@ class RecipesListScreenState extends State { ); } - Widget _buildRecipesShortScreen(List data) { + Widget _buildRecipesShortScreen(Iterable data) { return Padding( padding: const EdgeInsets.all(8.0), child: ListView.separated( itemCount: data.length, itemBuilder: (context, index) { - return _buildRecipeShortScreen(data[index]); + return _buildRecipeShortScreen(data.elementAt(index)); }, separatorBuilder: (context, index) => const Divider( color: Colors.black, diff --git a/lib/src/services/authentication_provider.dart b/lib/src/services/authentication_provider.dart index aa774871..febf7382 100644 --- a/lib/src/services/authentication_provider.dart +++ b/lib/src/services/authentication_provider.dart @@ -80,7 +80,7 @@ class AuthenticationProvider { .text; } on XmlParserException catch (e) { throw translate("login.errors.parse_failed", args: {"error_msg": e}); - // ignore: avoid_catching_errors + // ignore: avoid_catching_errors } on StateError catch (e) { throw translate("login.errors.parse_missing", args: {"error_msg": e}); } diff --git a/lib/src/widget/checkbox_form_field.dart b/lib/src/widget/checkbox_form_field.dart index d8b03185..1ce967b7 100644 --- a/lib/src/widget/checkbox_form_field.dart +++ b/lib/src/widget/checkbox_form_field.dart @@ -28,3 +28,4 @@ class CheckboxFormField extends FormField { }, ); } + \ No newline at end of file diff --git a/lib/src/widget/input/duration_form_field.dart b/lib/src/widget/input/duration_form_field.dart index 06b55935..b6d54616 100644 --- a/lib/src/widget/input/duration_form_field.dart +++ b/lib/src/widget/input/duration_form_field.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/input/integer_text_form_field.dart'; class DurationFormField extends StatefulWidget { @@ -23,10 +23,12 @@ class DurationFormField extends StatefulWidget { class _DurationFormFieldState extends State { late Duration currentDuration; + late bool enabled; @override void initState() { currentDuration = widget.duration; + enabled = widget.state.status != RecipeStatus.updateInProgress; super.initState(); } @@ -51,7 +53,7 @@ class _DurationFormFieldState extends State { SizedBox( width: 70, child: IntegerTextFormField( - enabled: widget.state is! RecipeUpdateInProgress, + enabled: enabled, initialValue: widget.duration.inHours, decoration: InputDecoration( hintText: translate('recipe.fields.time.hours'), @@ -72,7 +74,7 @@ class _DurationFormFieldState extends State { SizedBox( width: 50, child: IntegerTextFormField( - enabled: widget.state is! RecipeUpdateInProgress, + enabled: enabled, initialValue: widget.duration.inMinutes % 60, maxValue: 60, decoration: InputDecoration( diff --git a/lib/src/widget/input/list_form_field.dart b/lib/src/widget/input/list_form_field.dart index df89104c..0fc82b94 100644 --- a/lib/src/widget/input/list_form_field.dart +++ b/lib/src/widget/input/list_form_field.dart @@ -1,7 +1,7 @@ import 'dart:developer'; import 'package:flutter/material.dart'; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/my_reorderable_list.dart' as my; diff --git a/lib/src/widget/input/reorderable_list_form_field.dart b/lib/src/widget/input/reorderable_list_form_field.dart index da0b2091..b0855662 100644 --- a/lib/src/widget/input/reorderable_list_form_field.dart +++ b/lib/src/widget/input/reorderable_list_form_field.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_reorderable_list/flutter_reorderable_list.dart' as rl; -import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; class ReorderableListFormField extends StatefulWidget { final String title; @@ -32,6 +32,7 @@ class ItemData { class _ReorderableListFormFieldState extends State { final List _items = []; + late bool enabled; _ReorderableListFormFieldState(); @@ -56,6 +57,7 @@ class _ReorderableListFormFieldState extends State { for (int i = 0; i < widget.items.length; ++i) { _items.add(ItemData(widget.items[i], ValueKey(i))); } + enabled = widget.state.status != RecipeStatus.updateInProgress; super.initState(); } @@ -117,13 +119,11 @@ class _ReorderableListFormFieldState extends State { borderRadius: BorderRadius.circular(10), ), child: IconButton( - enableFeedback: - widget.state is! RecipeUpdateInProgress, + enableFeedback: enabled, icon: const Icon(Icons.add), onPressed: () { setState(() { - if (widget.state - is! RecipeUpdateInProgress) { + if (enabled) { _items.add( ItemData( "", @@ -210,6 +210,14 @@ class Item extends StatefulWidget { class _ItemState extends State { _ItemState(); + late bool enabled; + + @override + void initState() { + enabled = widget.state.status != RecipeStatus.updateInProgress; + super.initState(); + } + Widget _buildChild(BuildContext context, rl.ReorderableItemState state) { BoxDecoration decoration; @@ -239,7 +247,7 @@ class _ItemState extends State { // For iOS dragging mode, there will be drag handle on the right that triggers // reordering; For android mode it will be just an empty container final Widget dragHandle = rl.ReorderableListener( - canStart: () => widget.state is! RecipeUpdateInProgress, + canStart: () => enabled, child: Container( padding: const EdgeInsets.symmetric(horizontal: 7), color: const Color(0x08000000), @@ -253,10 +261,10 @@ class _ItemState extends State { color: const Color(0x08000000), child: Center( child: IconButton( - enableFeedback: widget.state is! RecipeUpdateInProgress, + enableFeedback: enabled, icon: const Icon(Icons.delete, color: Colors.red), onPressed: () { - if (widget.state is! RecipeUpdateInProgress) { + if (enabled) { widget.deleteItem(); } }, @@ -283,7 +291,7 @@ class _ItemState extends State { horizontal: 10.0, ), child: TextFormField( - enabled: widget.state is! RecipeUpdateInProgress, + enabled: enabled, maxLines: 10000, minLines: 1, initialValue: widget.data.text, diff --git a/pubspec.yaml b/pubspec.yaml index 04a65f5b..1bfaeb83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,7 @@ dependencies: http: ^0.13.1 # flutter_bloc for simpler bloc implementation - flutter_bloc: ^7.0.0 + flutter_bloc: ^8.1.1 # For easier handling without hand built hash comparison equatable: ^2.0.0 From d09d44448f5a684ba97699c3897cd1b372d35ee4 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Wed, 25 Jan 2023 16:58:02 +0100 Subject: [PATCH 02/11] Unify initial and loading states --- lib/main.dart | 13 ++++++------- .../blocs/authentication/authentication_bloc.dart | 5 ++--- .../blocs/authentication/authentication_state.dart | 3 +-- lib/src/blocs/categories/categories_bloc.dart | 1 - lib/src/blocs/categories/categories_state.dart | 4 +--- lib/src/blocs/recipe/recipe_bloc.dart | 1 - lib/src/blocs/recipe/recipe_state.dart | 5 +---- lib/src/screens/category/category_screen.dart | 1 - lib/src/screens/form/recipe_form.dart | 1 - lib/src/screens/loading_screen.dart | 11 ++--------- 10 files changed, 13 insertions(+), 32 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 8aee7106..d47b94e3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -107,14 +107,14 @@ class _AppState extends State { home: BlocBuilder( builder: (context, state) { switch (state.status) { - case AuthenticationStatus.uninitialized: + case AuthenticationStatus.loading: return const SplashPage(); case AuthenticationStatus.authenticated: IntentRepository().handleIntent(); - if (BlocProvider.of(context).state.status == - CategoriesStatus.initial) { - BlocProvider.of(context) - .add(const CategoriesLoaded()); + final categoryBloc = BlocProvider.of(context); + if (categoryBloc.state.status == + CategoriesStatus.loadInProgress) { + categoryBloc.add(const CategoriesLoaded()); } return const CategoryScreen(); case AuthenticationStatus.unauthenticated: @@ -123,9 +123,8 @@ class _AppState extends State { return const LoginScreen( invalidCredentials: true, ); - case AuthenticationStatus.loading: case AuthenticationStatus.error: - return const LoadingScreen(); + return const LoadingErrorScreen(); } }, ), diff --git a/lib/src/blocs/authentication/authentication_bloc.dart b/lib/src/blocs/authentication/authentication_bloc.dart index 78753a4d..26668d65 100644 --- a/lib/src/blocs/authentication/authentication_bloc.dart +++ b/lib/src/blocs/authentication/authentication_bloc.dart @@ -23,7 +23,6 @@ class AuthenticationBloc final bool hasToken = await userRepository.hasAppAuthentication(); if (hasToken) { - emit(AuthenticationState(status: AuthenticationStatus.loading)); await userRepository.loadAppAuthentication(); bool validCredentials = false; try { @@ -53,7 +52,7 @@ class AuthenticationBloc LoggedIn event, Emitter emit, ) async { - emit(AuthenticationState(status: AuthenticationStatus.loading)); + emit(AuthenticationState()); await userRepository.persistAppAuthentication(event.appAuthentication); await userRepository.fetchApiVersion(); emit(AuthenticationState(status: AuthenticationStatus.authenticated)); @@ -63,7 +62,7 @@ class AuthenticationBloc LoggedOut event, Emitter emit, ) async { - emit(AuthenticationState(status: AuthenticationStatus.loading)); + emit(AuthenticationState()); await userRepository.deleteAppAuthentication(); emit(AuthenticationState(status: AuthenticationStatus.unauthenticated)); } diff --git a/lib/src/blocs/authentication/authentication_state.dart b/lib/src/blocs/authentication/authentication_state.dart index 1ab56827..7899f347 100644 --- a/lib/src/blocs/authentication/authentication_state.dart +++ b/lib/src/blocs/authentication/authentication_state.dart @@ -2,7 +2,6 @@ part of 'authentication_bloc.dart'; enum AuthenticationStatus { unauthenticated, - uninitialized, authenticated, invalid, loading, @@ -14,7 +13,7 @@ class AuthenticationState extends Equatable { final String? error; const AuthenticationState({ - this.status = AuthenticationStatus.uninitialized, + this.status = AuthenticationStatus.loading, this.error, }) : assert( (status != AuthenticationStatus.error && error == null) || diff --git a/lib/src/blocs/categories/categories_bloc.dart b/lib/src/blocs/categories/categories_bloc.dart index 6e2bea50..e3b40c67 100644 --- a/lib/src/blocs/categories/categories_bloc.dart +++ b/lib/src/blocs/categories/categories_bloc.dart @@ -18,7 +18,6 @@ class CategoriesBloc extends Bloc { Emitter emit, ) async { try { - emit(CategoriesState(status: CategoriesStatus.loadInProgress)); final List categories = await dataRepository.fetchCategories(); dataRepository.updateCategoryNames(categories); emit( diff --git a/lib/src/blocs/categories/categories_state.dart b/lib/src/blocs/categories/categories_state.dart index c0c0b252..985129b0 100644 --- a/lib/src/blocs/categories/categories_state.dart +++ b/lib/src/blocs/categories/categories_state.dart @@ -1,7 +1,6 @@ part of 'categories_bloc.dart'; enum CategoriesStatus { - initial, loadInProgress, loadFailure, loadSuccess, @@ -14,12 +13,11 @@ class CategoriesState extends Equatable { final Iterable? categories; CategoriesState({ - this.status = CategoriesStatus.initial, + this.status = CategoriesStatus.loadInProgress, this.error, this.categories, }) { switch (status) { - case CategoriesStatus.initial: case CategoriesStatus.loadInProgress: assert(error == null && categories == null); break; diff --git a/lib/src/blocs/recipe/recipe_bloc.dart b/lib/src/blocs/recipe/recipe_bloc.dart index 44b1468a..ce617261 100644 --- a/lib/src/blocs/recipe/recipe_bloc.dart +++ b/lib/src/blocs/recipe/recipe_bloc.dart @@ -21,7 +21,6 @@ class RecipeBloc extends Bloc { Emitter emit, ) async { try { - emit(RecipeState(status: RecipeStatus.loadInProgress)); final recipe = await dataRepository.fetchRecipe(recipeLoaded.recipeId); emit(RecipeState(status: RecipeStatus.loadSuccess, recipe: recipe)); } catch (e) { diff --git a/lib/src/blocs/recipe/recipe_state.dart b/lib/src/blocs/recipe/recipe_state.dart index a0ceac83..3294da75 100644 --- a/lib/src/blocs/recipe/recipe_state.dart +++ b/lib/src/blocs/recipe/recipe_state.dart @@ -1,7 +1,6 @@ part of 'recipe_bloc.dart'; enum RecipeStatus { - initial, failure, success, loadSuccess, @@ -25,14 +24,12 @@ class RecipeState extends Equatable { final String? recipeId; RecipeState({ - this.status = RecipeStatus.initial, + this.status = RecipeStatus.loadInProgress, this.error, this.recipe, this.recipeId, }) { switch (status) { - case RecipeStatus.initial: - case RecipeStatus.loadInProgress: case RecipeStatus.updateInProgress: case RecipeStatus.createInProgress: diff --git a/lib/src/screens/category/category_screen.dart b/lib/src/screens/category/category_screen.dart index 6cd914cc..85c98d99 100644 --- a/lib/src/screens/category/category_screen.dart +++ b/lib/src/screens/category/category_screen.dart @@ -237,7 +237,6 @@ class _CategoryScreenState extends State { case CategoriesStatus.imageLoadSuccess: return _buildCategoriesScreen(categoriesState.categories!); case CategoriesStatus.loadInProgress: - case CategoriesStatus.initial: return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/src/screens/form/recipe_form.dart b/lib/src/screens/form/recipe_form.dart index e5956f04..67ceaf18 100644 --- a/lib/src/screens/form/recipe_form.dart +++ b/lib/src/screens/form/recipe_form.dart @@ -271,7 +271,6 @@ class _RecipeFormState extends State { case RecipeStatus.loadSuccess: case RecipeStatus.createSuccess: case RecipeStatus.createFailure: - case RecipeStatus.initial: default: return Text(widget.buttonSubmitText); } diff --git a/lib/src/screens/loading_screen.dart b/lib/src/screens/loading_screen.dart index 51d00cc4..12f3d33d 100644 --- a/lib/src/screens/loading_screen.dart +++ b/lib/src/screens/loading_screen.dart @@ -1,23 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; -class LoadingScreen extends StatelessWidget { - const LoadingScreen({super.key}); +class LoadingErrorScreen extends StatelessWidget { + const LoadingErrorScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: BlocBuilder( builder: (context, authenticationState) { - if (authenticationState.status != AuthenticationStatus.error) { - return SpinKitWave( - color: Theme.of(context).primaryColor, - ); - } - return Padding( padding: const EdgeInsets.all(8.0), child: Column( From bc23db8a536f48e4afbc76443e4803dc65dc83f4 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 30 Mar 2023 10:50:46 +0200 Subject: [PATCH 03/11] upgrade to dio 5.0 --- lib/src/services/authentication_provider.dart | 35 +++++++++---------- pubspec.yaml | 2 +- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/src/services/authentication_provider.dart b/lib/src/services/authentication_provider.dart index febf7382..a9bf1bb6 100644 --- a/lib/src/services/authentication_provider.dart +++ b/lib/src/services/authentication_provider.dart @@ -1,9 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; -import 'package:dio/adapter.dart'; import 'package:dio/dio.dart' as dio; +import 'package:dio/io.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_translate/flutter_translate.dart'; @@ -35,12 +34,12 @@ class AuthenticationProvider { try { final dio.Dio client = dio.Dio(); if (isSelfSignedCertificate) { - (client.httpClientAdapter as DefaultHttpClientAdapter) - .onHttpClientCreate = (HttpClient httpClient) { - httpClient.badCertificateCallback = - (X509Certificate cert, String host, int port) => true; - return null; - }; + client.httpClientAdapter = IOHttpClientAdapter( + onHttpClientCreate: (client) { + client.badCertificateCallback = (cert, host, port) => true; + return client; + }, + ); } response = await client.get( @@ -56,12 +55,12 @@ class AuthenticationProvider { cancelToken: _cancelToken, ); } on dio.DioError catch (e) { - if (e.message.contains("SocketException")) { + if (e.message?.contains("SocketException") ?? false) { throw translate( "login.errors.not_reachable", args: {"server_url": url, "error_msg": e}, ); - } else if (e.message.contains("CERTIFICATE_VERIFY_FAILED")) { + } else if (e.message?.contains("CERTIFICATE_VERIFY_FAILED") ?? false) { throw translate( "login.errors.certificate_failed", args: {"server_url": url, "error_msg": e}, @@ -126,12 +125,12 @@ class AuthenticationProvider { isSelfSignedCertificate: isSelfSignedCertificate, ); } on dio.DioError catch (e) { - if (e.message.contains("SocketException")) { + if (e.message?.contains("SocketException") ?? false) { throw translate( "login.errors.not_reachable", args: {"server_url": url, "error_msg": e}, ); - } else if (e.message.contains("CERTIFICATE_VERIFY_FAILED")) { + } else if (e.message?.contains("CERTIFICATE_VERIFY_FAILED") ?? false) { throw translate( "login.errors.certificate_failed", args: {"server_url": url, "error_msg": e}, @@ -191,12 +190,12 @@ class AuthenticationProvider { try { final dio.Dio client = dio.Dio(); if (isSelfSignedCertificate) { - (client.httpClientAdapter as DefaultHttpClientAdapter) - .onHttpClientCreate = (HttpClient httpClient) { - httpClient.badCertificateCallback = - (X509Certificate cert, String host, int port) => true; - return null; - }; + client.httpClientAdapter = IOHttpClientAdapter( + onHttpClientCreate: (client) { + client.badCertificateCallback = (cert, host, port) => true; + return client; + }, + ); } response = await client.get( urlAuthCheck, diff --git a/pubspec.yaml b/pubspec.yaml index 1bfaeb83..4b542658 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: theme_mode_handler: ^3.0.0 # Form data HTTP Client - dio: ^4.0.0 + dio: ^5.0.0 # Screen always on wakelock: ^0.6.1 From fd6b99de5b6660073443594f99b532d9d0bfd2fc Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 30 Mar 2023 11:13:14 +0200 Subject: [PATCH 04/11] upgrade other deps --- pubspec.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4b542658..b2b895d2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: punycode: ^1.0.0 #for app key - flutter_secure_storage: ^7.0.1 + flutter_secure_storage: ^8.0.0 url_launcher: ^6.0.15 # The following adds the Cupertino Icons font to your application. @@ -72,12 +72,12 @@ dependencies: flutter_spinkit: ^5.0.0 validators: ^3.0.0 - flutter_svg: ^1.0.3 + flutter_svg: ^2.0.4 flutter_markdown: ^0.6.9 # Reorderable List for Edit and Create Recipe - flutter_reorderable_list: 1.2.0 + flutter_reorderable_list: ^1.3.0 # Timer for cooking time flutter_local_notifications: ^13.0.0 @@ -87,12 +87,12 @@ dependencies: cached_network_image: ^3.0.0 flutter_cache_manager: ^3.3.0 - flutter_typeahead: 4.1.1 + flutter_typeahead: ^4.3.7 copy_with_extension: ^5.0.0 dev_dependencies: - flutter_launcher_icons: ^0.11.0 + flutter_launcher_icons: ^0.12.0 copy_with_extension_gen: ^5.0.0 build_runner: ^2.3.0 From 840d4e12246b7b6112e58ad128271149155bf59c Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 30 Mar 2023 13:16:47 +0200 Subject: [PATCH 05/11] migrate to flutter 3.7 --- .github/workflows/build.yml | 4 ++-- lib/main.dart | 1 - lib/src/screens/my_settings_screen.dart | 2 +- lib/src/screens/recipe/recipe_screen.dart | 6 +++--- lib/src/screens/recipe/widget/instruction_list.dart | 2 +- lib/src/screens/recipe/widget/nutrition_list_item.dart | 2 +- .../authentication_cached_network_recipe_image.dart | 1 - lib/src/widget/checkbox_form_field.dart | 5 +++-- lib/src/widget/duration_indicator.dart | 2 +- lib/src/widget/my_reorderable_list.dart | 10 ++++------ pubspec.yaml | 4 ++-- 11 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bda96bf..de830fc5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,8 +12,8 @@ jobs: with: java-version: '12.x' distribution: 'adopt' - - uses: subosito/flutter-action@v2.8.0 + - uses: subosito/flutter-action@v2.10.0 with: - flutter-version: '3.3.0' + channel: 'stable' - run: flutter pub get - run: flutter build apk --debug diff --git a/lib/main.dart b/lib/main.dart index d47b94e3..13f30da0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -98,7 +98,6 @@ class _AppState extends State { theme: ThemeData( brightness: Brightness.light, hintColor: Colors.grey, - backgroundColor: Colors.grey[400], ), darkTheme: ThemeData( brightness: Brightness.dark, diff --git a/lib/src/screens/my_settings_screen.dart b/lib/src/screens/my_settings_screen.dart index d4d9e396..8a389c49 100644 --- a/lib/src/screens/my_settings_screen.dart +++ b/lib/src/screens/my_settings_screen.dart @@ -28,7 +28,7 @@ class _MySettingsScreenState extends State { SliderSettingsTile( title: translate("settings.recipe_font_size.title"), settingKey: SettingKeys.recipe_font_size.name, - defaultValue: Theme.of(context).textTheme.bodyText2!.fontSize!, + defaultValue: Theme.of(context).textTheme.bodyMedium!.fontSize!, min: 10, max: 25, eagerUpdate: false, diff --git a/lib/src/screens/recipe/recipe_screen.dart b/lib/src/screens/recipe/recipe_screen.dart index 73bef39d..d04acc16 100644 --- a/lib/src/screens/recipe/recipe_screen.dart +++ b/lib/src/screens/recipe/recipe_screen.dart @@ -160,7 +160,7 @@ class RecipeScreenState extends State { final TextStyle settingsBasedTextStyle = TextStyle( fontSize: Settings.getValue( SettingKeys.recipe_font_size.name, - defaultValue: Theme.of(context).textTheme.bodyText2?.fontSize, + defaultValue: Theme.of(context).textTheme.bodyMedium?.fontSize, ), ); @@ -210,14 +210,14 @@ class RecipeScreenState extends State { text: translate('recipe.fields.servings'), style: Theme.of(context) .textTheme - .bodyText2! + .bodyMedium! .apply(fontWeightDelta: 3), children: [ TextSpan( text: " ${recipe.recipeYield}", style: Theme.of(context) .textTheme - .bodyText2! + .bodyMedium! .apply(fontWeightDelta: 3), ) ], diff --git a/lib/src/screens/recipe/widget/instruction_list.dart b/lib/src/screens/recipe/widget/instruction_list.dart index b6d3cd12..49a806fa 100644 --- a/lib/src/screens/recipe/widget/instruction_list.dart +++ b/lib/src/screens/recipe/widget/instruction_list.dart @@ -61,7 +61,7 @@ class _InstructionListState extends State { ), color: _instructionsDone[index] ? Colors.green - : Theme.of(context).backgroundColor, + : Theme.of(context).colorScheme.background, ), child: _instructionsDone[index] ? const Icon(Icons.check) diff --git a/lib/src/screens/recipe/widget/nutrition_list_item.dart b/lib/src/screens/recipe/widget/nutrition_list_item.dart index 974817ca..5e939238 100644 --- a/lib/src/screens/recipe/widget/nutrition_list_item.dart +++ b/lib/src/screens/recipe/widget/nutrition_list_item.dart @@ -17,7 +17,7 @@ class NutritionListItem extends StatelessWidget { Container( height: 30, decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.only( topLeft: Radius.circular(3), topRight: Radius.circular(3), diff --git a/lib/src/widget/authentication_cached_network_recipe_image.dart b/lib/src/widget/authentication_cached_network_recipe_image.dart index b4e20d82..2ee7a751 100644 --- a/lib/src/widget/authentication_cached_network_recipe_image.dart +++ b/lib/src/widget/authentication_cached_network_recipe_image.dart @@ -36,7 +36,6 @@ class AuthenticationCachedNetworkRecipeImage extends StatelessWidget { boxFit: boxFit, errorWidget: SvgPicture.asset( 'assets/icon.svg', - color: Colors.grey[600], ), ); } diff --git a/lib/src/widget/checkbox_form_field.dart b/lib/src/widget/checkbox_form_field.dart index 1ce967b7..a494a489 100644 --- a/lib/src/widget/checkbox_form_field.dart +++ b/lib/src/widget/checkbox_form_field.dart @@ -19,7 +19,9 @@ class CheckboxFormField extends FormField { ? Builder( builder: (BuildContext context) => Text( state.errorText!, - style: TextStyle(color: Theme.of(context).errorColor), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), ), ) : null, @@ -28,4 +30,3 @@ class CheckboxFormField extends FormField { }, ); } - \ No newline at end of file diff --git a/lib/src/widget/duration_indicator.dart b/lib/src/widget/duration_indicator.dart index 869d8354..f05635cf 100644 --- a/lib/src/widget/duration_indicator.dart +++ b/lib/src/widget/duration_indicator.dart @@ -18,7 +18,7 @@ class DurationIndicator extends StatelessWidget { Container( height: 35, decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.only( topLeft: Radius.circular(3), topRight: Radius.circular(3), diff --git a/lib/src/widget/my_reorderable_list.dart b/lib/src/widget/my_reorderable_list.dart index 6a75b614..ee17840f 100644 --- a/lib/src/widget/my_reorderable_list.dart +++ b/lib/src/widget/my_reorderable_list.dart @@ -270,9 +270,8 @@ class _ReorderableListContentState extends State<_ReorderableListContent> @override void didChangeDependencies() { - _scrollController = widget.scrollController ?? - PrimaryScrollController.of(context) ?? - ScrollController(); + _scrollController = + widget.scrollController ?? PrimaryScrollController.of(context); super.didChangeDependencies(); } @@ -310,7 +309,7 @@ class _ReorderableListContentState extends State<_ReorderableListContent> if (_scrolling) return; final contextObject = context.findRenderObject(); final viewport = RenderAbstractViewport.of(contextObject); - assert(contextObject != null && viewport != null); + assert(contextObject != null); // If and only if the current scroll offset falls in-between the offsets // necessary to reveal the selected context at the top or bottom of the // screen, then it is already on-screen. @@ -318,7 +317,7 @@ class _ReorderableListContentState extends State<_ReorderableListContent> final double scrollOffset = _scrollController.offset; final double topOffset = max( _scrollController.position.minScrollExtent, - viewport!.getOffsetToReveal(contextObject!, 0.0).offset - margin, + viewport.getOffsetToReveal(contextObject!, 0.0).offset - margin, ); final double bottomOffset = min( _scrollController.position.maxScrollExtent, @@ -482,7 +481,6 @@ class _ReorderableListContentState extends State<_ReorderableListContent> ), childWhenDragging: const SizedBox(), onDragStarted: onDragStarted, - dragAnchorStrategy: childDragAnchorStrategy, // When the drag ends inside a DragTarget widget, the drag // succeeds, and we reorder the widget into position appropriately. onDragCompleted: onDragEnded, diff --git a/pubspec.yaml b/pubspec.yaml index b2b895d2..3e885a36 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,8 +18,8 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 0.7.9+24 environment: - sdk: ">=2.17.0 <3.0.0" - flutter: 3.3.0 + sdk: ">=2.19.0 <3.0.0" + flutter: 3.7.0 dependencies: flutter: From 32f409940885a9aaf0fd54b75462e93dbfd093c3 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 30 Mar 2023 11:30:53 +0200 Subject: [PATCH 06/11] rename RecipeShort --- lib/src/blocs/recipes_short/recipes_short_state.dart | 2 +- lib/src/models/recipe_short.dart | 10 +++++----- lib/src/screens/category/category_screen.dart | 2 +- lib/src/screens/recipes_list_screen.dart | 12 ++++++------ .../services/category_recipes_short_provider.dart | 4 ++-- lib/src/services/data_repository.dart | 6 +++--- lib/src/services/recipes_short_provider.dart | 4 ++-- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/src/blocs/recipes_short/recipes_short_state.dart b/lib/src/blocs/recipes_short/recipes_short_state.dart index bfc6f308..b7b277b1 100644 --- a/lib/src/blocs/recipes_short/recipes_short_state.dart +++ b/lib/src/blocs/recipes_short/recipes_short_state.dart @@ -12,7 +12,7 @@ enum RecipesShortStatus { class RecipesShortState extends Equatable { final RecipesShortStatus status; final String? error; - final Iterable? recipesShort; + final Iterable? recipesShort; RecipesShortState({ this.status = RecipesShortStatus.loadInProgress, diff --git a/lib/src/models/recipe_short.dart b/lib/src/models/recipe_short.dart index 7102cc1b..635fbb9e 100644 --- a/lib/src/models/recipe_short.dart +++ b/lib/src/models/recipe_short.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:equatable/equatable.dart'; -class RecipeShort extends Equatable { +class RecipeStub extends Equatable { final String _recipeId; final String _name; final String _imageUrl; @@ -11,19 +11,19 @@ class RecipeShort extends Equatable { String get name => _name; String get imageUrl => _imageUrl; - RecipeShort.fromJson(Map json) + RecipeStub.fromJson(Map json) : _recipeId = json["recipe_id"] is int ? json["recipe_id"]!.toString() : json["recipe_id"] as String, _name = json["name"] as String, _imageUrl = json["imageUrl"] as String; - static List parseRecipesShort(String responseBody) { + static List parseRecipesShort(String responseBody) { final parsed = json.decode(responseBody) as List; return parsed - .map( - (json) => RecipeShort.fromJson(json as Map), + .map( + (json) => RecipeStub.fromJson(json as Map), ) .toList(); } diff --git a/lib/src/screens/category/category_screen.dart b/lib/src/screens/category/category_screen.dart index 85c98d99..532fe79f 100644 --- a/lib/src/screens/category/category_screen.dart +++ b/lib/src/screens/category/category_screen.dart @@ -144,7 +144,7 @@ class _CategoryScreenState extends State { if (state.status == RecipesShortStatus.loadAllSuccess) { showSearch( context: context, - delegate: SearchPage( + delegate: SearchPage( items: state.recipesShort!.toList(), searchLabel: translate('search.title'), suggestion: const Center( diff --git a/lib/src/screens/recipes_list_screen.dart b/lib/src/screens/recipes_list_screen.dart index 6a39b239..fc2d47c3 100644 --- a/lib/src/screens/recipes_list_screen.dart +++ b/lib/src/screens/recipes_list_screen.dart @@ -77,13 +77,13 @@ class RecipesListScreenState extends State { ); } - Widget _buildRecipesShortScreen(Iterable data) { + Widget _buildRecipesShortScreen(Iterable data) { return Padding( padding: const EdgeInsets.all(8.0), child: ListView.separated( itemCount: data.length, itemBuilder: (context, index) { - return _buildRecipeShortScreen(data.elementAt(index)); + return _buildRecipeStubScreen(data.elementAt(index)); }, separatorBuilder: (context, index) => const Divider( color: Colors.black, @@ -92,11 +92,11 @@ class RecipesListScreenState extends State { ); } - ListTile _buildRecipeShortScreen(RecipeShort recipeShort) { + ListTile _buildRecipeStubScreen(RecipeStub recipe) { return ListTile( - title: Text(recipeShort.name), + title: Text(recipe.name), trailing: AuthenticationCachedNetworkRecipeImage( - recipeId: recipeShort.recipeId, + recipeId: recipe.recipeId, full: false, width: 60, height: 60, @@ -105,7 +105,7 @@ class RecipesListScreenState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => RecipeScreen(recipeId: recipeShort.recipeId), + builder: (context) => RecipeScreen(recipeId: recipe.recipeId), ), ); }, diff --git a/lib/src/services/category_recipes_short_provider.dart b/lib/src/services/category_recipes_short_provider.dart index d0ef5a7e..b9e70a4d 100644 --- a/lib/src/services/category_recipes_short_provider.dart +++ b/lib/src/services/category_recipes_short_provider.dart @@ -5,7 +5,7 @@ import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; import 'package:nextcloud_cookbook_flutter/src/services/version_provider.dart'; class CategoryRecipesShortProvider { - Future> fetchCategoryRecipesShort(String category) async { + Future> fetchCategoryRecipesShort(String category) async { final AndroidApiVersion androidApiVersion = UserRepository().getAndroidVersion(); @@ -25,7 +25,7 @@ class CategoryRecipesShortProvider { // Parse categories try { final String contents = await Network().get(url); - return RecipeShort.parseRecipesShort(contents); + return RecipeStub.parseRecipesShort(contents); } catch (e) { throw Exception(e); } diff --git a/lib/src/services/data_repository.dart b/lib/src/services/data_repository.dart index 5470042a..c9317aba 100644 --- a/lib/src/services/data_repository.dart +++ b/lib/src/services/data_repository.dart @@ -31,7 +31,7 @@ class DataRepository { static String categoryAll = translate('categories.all_categories'); // Actions - Future> fetchRecipesShort({required String category}) { + Future> fetchRecipesShort({required String category}) { if (category == categoryAll) { return recipesShortProvider.fetchRecipesShort(); } else { @@ -71,7 +71,7 @@ class DataRepository { } Future _fetchCategoryMainRecipe(Category category) async { - List categoryRecipes = []; + List categoryRecipes = []; try { if (category.name == translate('categories.all_categories')) { @@ -91,7 +91,7 @@ class DataRepository { return category; } - Future> fetchAllRecipes() async { + Future> fetchAllRecipes() async { return fetchRecipesShort(category: categoryAll); } diff --git a/lib/src/services/recipes_short_provider.dart b/lib/src/services/recipes_short_provider.dart index 5d8fa7c7..b452ae5a 100644 --- a/lib/src/services/recipes_short_provider.dart +++ b/lib/src/services/recipes_short_provider.dart @@ -5,7 +5,7 @@ import 'package:nextcloud_cookbook_flutter/src/services/network.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; class RecipesShortProvider { - Future> fetchRecipesShort() async { + Future> fetchRecipesShort() async { final AppAuthentication appAuthentication = UserRepository().currentAppAuthentication; @@ -13,7 +13,7 @@ class RecipesShortProvider { "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes"; try { final String contents = await Network().get(url); - return RecipeShort.parseRecipesShort(contents); + return RecipeStub.parseRecipesShort(contents); } catch (e) { throw Exception(translate('recipe_list.errors.load_failed')); } From d8fa2b7aa1ced71b932e6ccab0c5aa7c8a0fda7d Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 30 Mar 2023 11:41:04 +0200 Subject: [PATCH 07/11] bundle all services --- lib/main.dart | 4 +- .../authentication/authentication_bloc.dart | 2 +- lib/src/blocs/categories/categories_bloc.dart | 2 +- lib/src/blocs/login/login_bloc.dart | 2 +- lib/src/blocs/recipe/recipe_bloc.dart | 2 +- .../recipes_short/recipes_short_bloc.dart | 2 +- lib/src/models/timer.dart | 2 +- lib/src/screens/category/category_screen.dart | 2 +- lib/src/screens/form/login_form.dart | 2 +- lib/src/screens/form/recipe_form.dart | 2 +- lib/src/services/authentication_provider.dart | 11 +----- lib/src/services/categories_provider.dart | 6 +-- .../category_recipes_short_provider.dart | 6 +-- .../services/category_search_provider.dart | 3 +- lib/src/services/data_repository.dart | 13 +------ lib/src/services/intent_repository.dart | 4 +- .../services/net/nextcloud_metadata_api.dart | 3 +- lib/src/services/network.dart | 5 +-- lib/src/services/notification_provider.dart | 8 +--- lib/src/services/recipe_provider.dart | 7 +--- lib/src/services/recipes_short_provider.dart | 6 +-- lib/src/services/services.dart | 38 +++++++++++++++++++ lib/src/services/user_repository.dart | 7 +--- lib/src/services/version_provider.dart | 5 +-- lib/src/util/custom_cache_manager.dart | 2 +- lib/src/widget/api_version_warning.dart | 3 +- .../authentication_cached_network_image.dart | 2 +- ...ntication_cached_network_recipe_image.dart | 2 +- 28 files changed, 65 insertions(+), 88 deletions(-) create mode 100644 lib/src/services/services.dart diff --git a/lib/main.dart b/lib/main.dart index 13f30da0..91e354e5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,9 +10,7 @@ import 'package:nextcloud_cookbook_flutter/src/screens/category/category_screen. import 'package:nextcloud_cookbook_flutter/src/screens/loading_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/login_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/splash_screen.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/intent_repository.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/notification_provider.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/util/lifecycle_event_handler.dart'; import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; import 'package:nextcloud_cookbook_flutter/src/util/supported_locales.dart'; diff --git a/lib/src/blocs/authentication/authentication_bloc.dart b/lib/src/blocs/authentication/authentication_bloc.dart index 26668d65..b9e076bb 100644 --- a/lib/src/blocs/authentication/authentication_bloc.dart +++ b/lib/src/blocs/authentication/authentication_bloc.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; part 'authentication_event.dart'; part 'authentication_state.dart'; diff --git a/lib/src/blocs/categories/categories_bloc.dart b/lib/src/blocs/categories/categories_bloc.dart index e3b40c67..2d41ef5b 100644 --- a/lib/src/blocs/categories/categories_bloc.dart +++ b/lib/src/blocs/categories/categories_bloc.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; part 'categories_event.dart'; part 'categories_state.dart'; diff --git a/lib/src/blocs/login/login_bloc.dart b/lib/src/blocs/login/login_bloc.dart index 3901ba57..992c9f6e 100644 --- a/lib/src/blocs/login/login_bloc.dart +++ b/lib/src/blocs/login/login_bloc.dart @@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; part 'login_event.dart'; part 'login_state.dart'; diff --git a/lib/src/blocs/recipe/recipe_bloc.dart b/lib/src/blocs/recipe/recipe_bloc.dart index ce617261..6e46932f 100644 --- a/lib/src/blocs/recipe/recipe_bloc.dart +++ b/lib/src/blocs/recipe/recipe_bloc.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; part 'recipe_event.dart'; part 'recipe_state.dart'; diff --git a/lib/src/blocs/recipes_short/recipes_short_bloc.dart b/lib/src/blocs/recipes_short/recipes_short_bloc.dart index 9a8b0149..d74747f4 100644 --- a/lib/src/blocs/recipes_short/recipes_short_bloc.dart +++ b/lib/src/blocs/recipes_short/recipes_short_bloc.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; part 'recipes_short_event.dart'; part 'recipes_short_state.dart'; diff --git a/lib/src/models/timer.dart b/lib/src/models/timer.dart index cd93ec82..0a95a7bb 100644 --- a/lib/src/models/timer.dart +++ b/lib/src/models/timer.dart @@ -1,4 +1,4 @@ -import 'package:nextcloud_cookbook_flutter/src/services/notification_provider.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:timezone/timezone.dart' as tz; class TimerList { diff --git a/lib/src/screens/category/category_screen.dart b/lib/src/screens/category/category_screen.dart index 532fe79f..b2079d38 100644 --- a/lib/src/screens/category/category_screen.dart +++ b/lib/src/screens/category/category_screen.dart @@ -15,7 +15,7 @@ import 'package:nextcloud_cookbook_flutter/src/screens/recipe_create_screen.dart import 'package:nextcloud_cookbook_flutter/src/screens/recipe_import_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipes_list_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/timer_screen.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/api_version_warning.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_image.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; diff --git a/lib/src/screens/form/login_form.dart b/lib/src/screens/form/login_form.dart index def61385..95901011 100644 --- a/lib/src/screens/form/login_form.dart +++ b/lib/src/screens/form/login_form.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/login/login_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/checkbox_form_field.dart'; import 'package:punycode/punycode.dart'; diff --git a/lib/src/screens/form/recipe_form.dart b/lib/src/screens/form/recipe_form.dart index 67ceaf18..c868c51e 100644 --- a/lib/src/screens/form/recipe_form.dart +++ b/lib/src/screens/form/recipe_form.dart @@ -5,7 +5,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/input/duration_form_field.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/input/integer_text_form_field.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/input/reorderable_list_form_field.dart'; diff --git a/lib/src/services/authentication_provider.dart b/lib/src/services/authentication_provider.dart index a9bf1bb6..66a676e2 100644 --- a/lib/src/services/authentication_provider.dart +++ b/lib/src/services/authentication_provider.dart @@ -1,13 +1,4 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:dio/dio.dart' as dio; -import 'package:dio/io.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:xml/xml.dart'; +part of 'services.dart'; class AuthenticationProvider { final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); diff --git a/lib/src/services/categories_provider.dart b/lib/src/services/categories_provider.dart index 55d3ea34..088212fe 100644 --- a/lib/src/services/categories_provider.dart +++ b/lib/src/services/categories_provider.dart @@ -1,8 +1,4 @@ -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/network.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +part of 'services.dart'; class CategoriesProvider { Future> fetchCategories() async { diff --git a/lib/src/services/category_recipes_short_provider.dart b/lib/src/services/category_recipes_short_provider.dart index b9e70a4d..51f4f585 100644 --- a/lib/src/services/category_recipes_short_provider.dart +++ b/lib/src/services/category_recipes_short_provider.dart @@ -1,8 +1,4 @@ -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/network.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/version_provider.dart'; +part of 'services.dart'; class CategoryRecipesShortProvider { Future> fetchCategoryRecipesShort(String category) async { diff --git a/lib/src/services/category_search_provider.dart b/lib/src/services/category_search_provider.dart index 8b4b2de6..9742188b 100644 --- a/lib/src/services/category_search_provider.dart +++ b/lib/src/services/category_search_provider.dart @@ -1,5 +1,4 @@ -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; +part of 'services.dart'; class CategorySearchProvider { List categoryNames = []; diff --git a/lib/src/services/data_repository.dart b/lib/src/services/data_repository.dart index c9317aba..2639b942 100644 --- a/lib/src/services/data_repository.dart +++ b/lib/src/services/data_repository.dart @@ -1,15 +1,4 @@ -import 'dart:developer'; - -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/categories_provider.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/category_recipes_short_provider.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/category_search_provider.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/net/nextcloud_metadata_api.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/recipe_provider.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/recipes_short_provider.dart'; +part of 'services.dart'; class DataRepository { // Singleton diff --git a/lib/src/services/intent_repository.dart b/lib/src/services/intent_repository.dart index 5f4e7a97..86f025c3 100644 --- a/lib/src/services/intent_repository.dart +++ b/lib/src/services/intent_repository.dart @@ -1,6 +1,4 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:nextcloud_cookbook_flutter/src/screens/recipe_import_screen.dart'; +part of 'services.dart'; class IntentRepository { // Singleton Pattern diff --git a/lib/src/services/net/nextcloud_metadata_api.dart b/lib/src/services/net/nextcloud_metadata_api.dart index 5a15df75..85795b3f 100644 --- a/lib/src/services/net/nextcloud_metadata_api.dart +++ b/lib/src/services/net/nextcloud_metadata_api.dart @@ -1,5 +1,4 @@ -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +part of '../services.dart'; class NextcloudMetadataApi { final AppAuthentication _appAuthentication; diff --git a/lib/src/services/network.dart b/lib/src/services/network.dart index d70b9a37..ee8fd6eb 100644 --- a/lib/src/services/network.dart +++ b/lib/src/services/network.dart @@ -1,7 +1,4 @@ -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; -import 'package:nextcloud_cookbook_flutter/src/util/custom_cache_manager.dart'; +part of 'services.dart'; class Network { static final Network _network = Network._(); diff --git a/lib/src/services/notification_provider.dart b/lib/src/services/notification_provider.dart index c6f4f9a6..f89a253c 100644 --- a/lib/src/services/notification_provider.dart +++ b/lib/src/services/notification_provider.dart @@ -1,10 +1,4 @@ -import 'dart:convert'; - -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; -import 'package:timezone/data/latest_10y.dart' as tz; -import 'package:timezone/timezone.dart' as tz; +part of 'services.dart'; const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails( diff --git a/lib/src/services/recipe_provider.dart b/lib/src/services/recipe_provider.dart index ad7ea668..3dd1e6a2 100644 --- a/lib/src/services/recipe_provider.dart +++ b/lib/src/services/recipe_provider.dart @@ -1,9 +1,4 @@ -import 'package:dio/dio.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/network.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +part of 'services.dart'; class RecipeProvider { Future fetchRecipe(int id) async { diff --git a/lib/src/services/recipes_short_provider.dart b/lib/src/services/recipes_short_provider.dart index b452ae5a..cea633da 100644 --- a/lib/src/services/recipes_short_provider.dart +++ b/lib/src/services/recipes_short_provider.dart @@ -1,8 +1,4 @@ -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/network.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +part of 'services.dart'; class RecipesShortProvider { Future> fetchRecipesShort() async { diff --git a/lib/src/services/services.dart b/lib/src/services/services.dart new file mode 100644 index 00000000..b7508378 --- /dev/null +++ b/lib/src/services/services.dart @@ -0,0 +1,38 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:dio/dio.dart' as dio; +import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; +import 'package:nextcloud_cookbook_flutter/src/screens/recipe_import_screen.dart'; +import 'package:nextcloud_cookbook_flutter/src/util/custom_cache_manager.dart'; +import 'package:timezone/data/latest_10y.dart' as tz; +import 'package:timezone/timezone.dart' as tz; +import 'package:xml/xml.dart'; + +part "authentication_provider.dart"; +part "categories_provider.dart"; +part "category_recipes_short_provider.dart"; +part "category_search_provider.dart"; +part "data_repository.dart"; +part "intent_repository.dart"; +part "net/nextcloud_metadata_api.dart"; +part "network.dart"; +part "notification_provider.dart"; +part "recipe_provider.dart"; +part "recipes_short_provider.dart"; +part "user_repository.dart"; +part "version_provider.dart"; diff --git a/lib/src/services/user_repository.dart b/lib/src/services/user_repository.dart index 00eaac35..11bb3621 100644 --- a/lib/src/services/user_repository.dart +++ b/lib/src/services/user_repository.dart @@ -1,9 +1,4 @@ -import 'dart:async'; - -import 'package:dio/dio.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/authentication_provider.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/version_provider.dart'; +part of 'services.dart'; class UserRepository { // Singleton diff --git a/lib/src/services/version_provider.dart b/lib/src/services/version_provider.dart index d011e298..8ae74448 100644 --- a/lib/src/services/version_provider.dart +++ b/lib/src/services/version_provider.dart @@ -1,7 +1,4 @@ -import 'dart:convert'; - -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +part of 'services.dart'; class VersionProvider { late ApiVersion _currentApiVersion; diff --git a/lib/src/util/custom_cache_manager.dart b/lib/src/util/custom_cache_manager.dart index 71426e59..175314c5 100644 --- a/lib/src/util/custom_cache_manager.dart +++ b/lib/src/util/custom_cache_manager.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:http/io_client.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; class CustomCacheManager { static final CustomCacheManager _instance = CustomCacheManager._(); diff --git a/lib/src/widget/api_version_warning.dart b/lib/src/widget/api_version_warning.dart index 68be49c9..5039ac79 100644 --- a/lib/src/widget/api_version_warning.dart +++ b/lib/src/widget/api_version_warning.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/version_provider.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; class ApiVersionWarning extends StatelessWidget { const ApiVersionWarning({super.key}); diff --git a/lib/src/widget/authentication_cached_network_image.dart b/lib/src/widget/authentication_cached_network_image.dart index 05efde33..e5806a12 100644 --- a/lib/src/widget/authentication_cached_network_image.dart +++ b/lib/src/widget/authentication_cached_network_image.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; class AuthenticationCachedNetworkImage extends StatelessWidget { final double? width; diff --git a/lib/src/widget/authentication_cached_network_recipe_image.dart b/lib/src/widget/authentication_cached_network_recipe_image.dart index 2ee7a751..d721743a 100644 --- a/lib/src/widget/authentication_cached_network_recipe_image.dart +++ b/lib/src/widget/authentication_cached_network_recipe_image.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_image.dart'; class AuthenticationCachedNetworkRecipeImage extends StatelessWidget { From d52ded1d5bfe107ec167024a6fbce1332e06e9a6 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Sat, 1 Apr 2023 11:35:49 +0200 Subject: [PATCH 08/11] refactor timer --- lib/src/models/recipe.dart | 11 +- lib/src/models/timer.dart | 173 ++++++++++-------- lib/src/models/timer.g.dart | 19 ++ lib/src/screens/recipe/recipe_screen.dart | 20 +- lib/src/screens/timer_screen.dart | 5 +- lib/src/services/notification_provider.dart | 20 +- lib/src/services/recipe_provider.dart | 8 +- lib/src/services/services.dart | 1 + lib/src/services/timer_repository.dart | 28 +++ .../widget/animated_time_progress_bar.dart | 6 +- pubspec.yaml | 2 + test/models/timer_test.dart | 45 +++++ 12 files changed, 224 insertions(+), 114 deletions(-) create mode 100644 lib/src/models/timer.g.dart create mode 100644 lib/src/services/timer_repository.dart create mode 100644 test/models/timer_test.dart diff --git a/lib/src/models/recipe.dart b/lib/src/models/recipe.dart index 887f523b..52fa62d8 100644 --- a/lib/src/models/recipe.dart +++ b/lib/src/models/recipe.dart @@ -23,9 +23,10 @@ class Recipe extends Equatable { final String url; final Map remainingData; - factory Recipe(String jsonString) { - final data = json.decode(jsonString) as Map; + factory Recipe.fromJsonString(String jsonString) => + Recipe.fromJson(json.decode(jsonString) as Map); + factory Recipe.fromJson(Map data) { final String id = data["id"] is int ? data["id"]!.toString() : data["id"] as String; final String name = data["name"] as String; @@ -173,7 +174,9 @@ class Recipe extends Equatable { ); } - String toJson() { + String toJsonString() => jsonEncode(toJson()); + + Map toJson() { final Map updatedData = { 'id': id, 'name': name, @@ -202,7 +205,7 @@ class Recipe extends Equatable { remainingData['nutrition'] = nutrition; } - return jsonEncode(remainingData); + return remainingData; } MutableRecipe toMutableRecipe() { diff --git a/lib/src/models/timer.dart b/lib/src/models/timer.dart index 0a95a7bb..12e46306 100644 --- a/lib/src/models/timer.dart +++ b/lib/src/models/timer.dart @@ -1,99 +1,112 @@ -import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; -import 'package:timezone/timezone.dart' as tz; +import 'package:flutter/foundation.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; -class TimerList { - static final TimerList _instance = TimerList._(); - final List timers; +part 'timer.g.dart'; - factory TimerList() => _instance; +@JsonSerializable(constructor: "restore") +class Timer { + @visibleForTesting + final Recipe? recipe; + final DateTime done; - TimerList._() : timers = []; + final String? _title; + final String? _body; + final Duration? _duration; + final String? _recipeId; - List get(String recipeId) { - final List l = []; - for (final value in timers) { - if (value.recipeId == recipeId) l.add(value); - } - return l; - } - - void clear() { - timers.clear(); - NotificationService().cancelAll(); - } -} + int? id; -class Timer { - final String? title; - final String body; - final Duration duration; - int id = 0; - final tz.TZDateTime done; - final String recipeId; - - Timer( - this.recipeId, - this.title, - this.body, - this.duration, - ) : done = tz.TZDateTime.now(tz.local).add(duration); + Timer(Recipe this.recipe) + : _title = null, + _body = null, + _duration = null, + _recipeId = null, + done = DateTime.now().add(recipe.cookTime); // Restore Timer fom pending notification - Timer._restore( - this.recipeId, - this.title, - this.body, - this.duration, + @visibleForTesting + Timer.restore( + Recipe this.recipe, this.done, this.id, - ); - - factory Timer.fromJson(Map json, int id) { - final Timer timer = Timer._restore( - json['recipeId'] is String - ? json['recipeId'] as String - : json['recipeId'].toString(), - json['title'] as String, - json['body'] as String, - Duration(minutes: json['duration'] as int), - tz.TZDateTime.fromMicrosecondsSinceEpoch(tz.local, json['done'] as int), - id, - ); - TimerList().timers.add(timer); - return timer; - } + ) : _title = null, + _body = null, + _duration = null, + _recipeId = null; - Map toJson() => { - 'title': title, - 'body': body, - 'duration': duration.inMinutes, - 'done': done.microsecondsSinceEpoch, - 'id': id, - 'recipeId': recipeId, - }; - - void start() { - NotificationService().start(this); - } + @visibleForTesting + Timer.restoreOld( + this.done, + this.id, + this._title, + this._body, + this._duration, + this._recipeId, + ) : recipe = null; - // cancel the timer - void cancel() { - NotificationService().cancel(this); - TimerList().timers.remove(this); + factory Timer.fromJson(Map json) { + try { + return _$TimerFromJson(json); + // ignore: avoid_catching_errors + } on TypeError { + return Timer.restoreOld( + DateTime.fromMicrosecondsSinceEpoch(json['done'] as int), + json['id'] as int?, + json['title'] as String, + json['body'] as String, + Duration(minutes: json['duration'] as int), + json['recipeId'] is String + ? json['recipeId'] as String + : json['recipeId'].toString(), + ); + } } - Duration remaining() { - if (done.difference(tz.TZDateTime.now(tz.local)).isNegative) { + Map toJson() => _$TimerToJson(this); + + String get body => _body ?? "$title ${translate('timer.finished')}"; + String get title => _title ?? recipe!.name; + Duration get duration => _duration ?? recipe!.cookTime; + String get recipeId => _recipeId ?? recipe!.id; + + /// The remaining time of the timer + /// + /// Returns [Duration.zero] when done. + Duration get remaining { + final difference = done.difference(DateTime.now()); + + if (difference.isNegative) { return Duration.zero; - } else { - return done.difference(tz.TZDateTime.now(tz.local)); } + + return difference; } - double progress() { - final Duration remainingTime = remaining(); - return remainingTime.inSeconds > 0 - ? 1 - (remainingTime.inSeconds / duration.inSeconds) - : 1.0; + /// Prgogrss of the timer in percent. + double get progress { + if (remaining == Duration.zero) { + return 1.0; + } + + return 1.0 - (remaining.inMicroseconds / duration.inMicroseconds); } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Timer && + runtimeType == other.runtimeType && + hashCode == other.hashCode; + + @override + int get hashCode => Object.hash( + done, + id, + title, + body, + duration, + recipeId, + ); } diff --git a/lib/src/models/timer.g.dart b/lib/src/models/timer.g.dart new file mode 100644 index 00000000..b59076f4 --- /dev/null +++ b/lib/src/models/timer.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'timer.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Timer _$TimerFromJson(Map json) => Timer.restore( + Recipe.fromJson(json['recipe'] as Map), + DateTime.parse(json['done'] as String), + json['id'] as int?, + ); + +Map _$TimerToJson(Timer instance) => { + 'recipe': instance.recipe, + 'done': instance.done.toIso8601String(), + 'id': instance.id, + }; diff --git a/lib/src/screens/recipe/recipe_screen.dart b/lib/src/screens/recipe/recipe_screen.dart index d04acc16..aa3d4ff9 100644 --- a/lib/src/screens/recipe/recipe_screen.dart +++ b/lib/src/screens/recipe/recipe_screen.dart @@ -10,6 +10,7 @@ import 'package:nextcloud_cookbook_flutter/src/screens/recipe/widget/ingredient_ import 'package:nextcloud_cookbook_flutter/src/screens/recipe/widget/instruction_list.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe/widget/nutrition_list.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe_edit_screen.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/animated_time_progress_bar.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; @@ -128,14 +129,7 @@ class RecipeScreenState extends State { onPressed: () { { if (enabled) { - final Timer timer = Timer( - recipe.id, - recipe.name, - "${recipe.name} ${translate('timer.finished')}", - recipe.cookTime, - ); - timer.start(); - TimerList().timers.add(timer); + TimerList().timers.add(Timer(recipe)); setState(() {}); final snackBar = SnackBar(content: Text(translate('timer.started'))); @@ -340,17 +334,17 @@ class RecipeScreenState extends State { } Widget _showTimers(Recipe recipe) { - final List l = TimerList().get(recipe.id); - if (l.isNotEmpty) { + final timers = TimerList().timers; + if (timers.isNotEmpty) { return Padding( padding: const EdgeInsets.only(bottom: 10.0), child: Column( children: [ ListView.builder( shrinkWrap: true, - itemCount: l.length, + itemCount: timers.length, itemBuilder: (context, index) { - return _buildTimerListItem(l[index]); + return _buildTimerListItem(timers[index]); }, ) ], @@ -369,7 +363,7 @@ class RecipeScreenState extends State { trailing: IconButton( icon: const Icon(Icons.cancel), onPressed: () { - timer.cancel(); + TimerList().remove(timer); setState(() {}); }, ), diff --git a/lib/src/screens/timer_screen.dart b/lib/src/screens/timer_screen.dart index 56e8ec5b..9d69e222 100644 --- a/lib/src/screens/timer_screen.dart +++ b/lib/src/screens/timer_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe/recipe_screen.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/animated_time_progress_bar.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; @@ -71,7 +72,7 @@ class _TimerScreen extends State { width: 60, height: 60, ), - title: Text(timer.title!), + title: Text(timer.title), subtitle: AnimatedTimeProgressBar( timer: timer, ), @@ -79,7 +80,7 @@ class _TimerScreen extends State { trailing: IconButton( icon: const Icon(Icons.cancel), onPressed: () { - timer.cancel(); + TimerList().remove(timer); setState(() {}); }, ), diff --git a/lib/src/services/notification_provider.dart b/lib/src/services/notification_provider.dart index f89a253c..e71a96c9 100644 --- a/lib/src/services/notification_provider.dart +++ b/lib/src/services/notification_provider.dart @@ -87,31 +87,35 @@ class NotificationService { for (final element in pendingNotificationRequests) { if (element.payload != null) { final data = jsonDecode(element.payload!) as Map; - final Timer timer = Timer.fromJson(data, element.id); - if (timer.id > curId) curId = timer.id; + final timer = Timer.fromJson(data)..id = element.id; + TimerList()._timers.add(timer); + if (timer.id! > curId) curId = timer.id!; } } } - int start(Timer timer) { - curId++; + void start(Timer timer) { timer.id = curId; + _localNotifications.zonedSchedule( - curId, + curId++, timer.title, timer.body, - timer.done, + tz.TZDateTime.from(timer.done, tz.local), platformChannelSpecifics, payload: jsonEncode(timer.toJson()), androidAllowWhileIdle: true, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.wallClockTime, ); - return curId; } void cancel(Timer timer) { - _localNotifications.cancel(timer.id); + assert( + timer.id != null, + "The timer should have an ID. If not it probably wasn't started", + ); + _localNotifications.cancel(timer.id!); } void cancelAll() { diff --git a/lib/src/services/recipe_provider.dart b/lib/src/services/recipe_provider.dart index 3dd1e6a2..d4155db2 100644 --- a/lib/src/services/recipe_provider.dart +++ b/lib/src/services/recipe_provider.dart @@ -10,7 +10,7 @@ class RecipeProvider { // Parse categories try { final String contents = await Network().get(url); - return Recipe(contents); + return Recipe.fromJsonString(contents); } catch (e) { throw Exception(e); } @@ -26,7 +26,7 @@ class RecipeProvider { "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes/${recipe.id}"; final response = await client.put( url, - data: recipe.toJson(), + data: recipe.toJsonString(), options: Options( contentType: "application/json;charset=UTF-8", ), @@ -47,7 +47,7 @@ class RecipeProvider { try { final response = await client.post( "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes", - data: recipe.toJson(), + data: recipe.toJsonString(), options: Options( contentType: "application/json;charset=UTF-8", ), @@ -72,7 +72,7 @@ class RecipeProvider { ), ); - return Recipe(response.data as String); + return Recipe.fromJsonString(response.data as String); } on DioError catch (e) { throw Exception(e.response); } catch (e) { diff --git a/lib/src/services/services.dart b/lib/src/services/services.dart index b7508378..a13f4aae 100644 --- a/lib/src/services/services.dart +++ b/lib/src/services/services.dart @@ -36,3 +36,4 @@ part "recipe_provider.dart"; part "recipes_short_provider.dart"; part "user_repository.dart"; part "version_provider.dart"; +part 'timer_repository.dart'; diff --git a/lib/src/services/timer_repository.dart b/lib/src/services/timer_repository.dart new file mode 100644 index 00000000..32342027 --- /dev/null +++ b/lib/src/services/timer_repository.dart @@ -0,0 +1,28 @@ +part of 'services.dart'; + +class TimerList { + static final TimerList _instance = TimerList._(); + final List _timers; + + factory TimerList() => _instance; + + TimerList._() : _timers = []; + + List get timers => _timers; + + void add(Timer timer) { + NotificationService().start(timer); + _timers.add(timer); + } + + void remove(Timer timer) { + NotificationService().cancel(timer); + _timers.remove(timer); + } + + void clear() { + for (final timer in _timers) { + remove(timer); + } + } +} diff --git a/lib/src/widget/animated_time_progress_bar.dart b/lib/src/widget/animated_time_progress_bar.dart index 6e9331c6..995571ac 100644 --- a/lib/src/widget/animated_time_progress_bar.dart +++ b/lib/src/widget/animated_time_progress_bar.dart @@ -28,12 +28,12 @@ class _AnimatedTimeProgressBarState extends State _timer = widget.timer; _timerTween = Tween( - begin: _timer.progress(), + begin: _timer.progress, end: 1.0, ); _controller = AnimationController( - duration: _timer.remaining(), + duration: _timer.remaining, vsync: this, ); @@ -63,7 +63,7 @@ class _AnimatedTimeProgressBarState extends State mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "${_timer.remaining().inHours.toString().padLeft(2, '0')}:${_timer.remaining().inMinutes.remainder(60).toString().padLeft(2, '0')}:${(_timer.remaining().inSeconds.remainder(60)).toString().padLeft(2, '0')}", + "${_timer.remaining.inHours.toString().padLeft(2, '0')}:${_timer.remaining.inMinutes.remainder(60).toString().padLeft(2, '0')}:${(_timer.remaining.inSeconds.remainder(60)).toString().padLeft(2, '0')}", ), Text( "${_timer.duration.inHours.toString().padLeft(2, '0')}:${_timer.duration.inMinutes.remainder(60).toString().padLeft(2, '0')}:${(_timer.duration.inSeconds.remainder(60)).toString().padLeft(2, '0')}", diff --git a/pubspec.yaml b/pubspec.yaml index 3e885a36..d4160638 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,11 +90,13 @@ dependencies: flutter_typeahead: ^4.3.7 copy_with_extension: ^5.0.0 + json_annotation: ^4.8.0 dev_dependencies: flutter_launcher_icons: ^0.12.0 copy_with_extension_gen: ^5.0.0 + json_serializable: ^6.6.0 build_runner: ^2.3.0 flutter_test: diff --git a/test/models/timer_test.dart b/test/models/timer_test.dart new file mode 100644 index 00000000..51c8b93b --- /dev/null +++ b/test/models/timer_test.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; + +void main() { + const title = "title"; + const body = "body"; + const duration = Duration(minutes: 5); + final done = DateTime.now().add(duration); + const recipeId = "12345678"; + const id = 0; + + final timer = Timer.restoreOld(done, id, recipeId, title, duration, recipeId); + + final json = + '{"title":"$title","body":"$body","duration":${duration.inMinutes},"done":${done.millisecondsSinceEpoch},"id":$id,"recipeId":"$recipeId"}'; + final orderedJson = + '{"title":"$title","body":"$body","duration":${duration.inMinutes},"id":$id,"done":${done.millisecondsSinceEpoch},"recipeId":"$recipeId"}'; + final oldJson = + '{"title":"$title","body":"$body","duration":${duration.inMinutes},"done":${done.millisecondsSinceEpoch},"id":$id,"recipeId":$recipeId}'; + + final newJson = '{"recipe":null,"done":"${done.toIso8601String()}","id":$id}'; + + group(Timer, () { + test("toJson", () { + expect(jsonEncode(timer.toJson()), equals(newJson)); + }); + + test("fromJson", () { + expect( + Timer.fromJson(jsonDecode(json) as Map), + isA(), + ); + expect( + Timer.fromJson(jsonDecode(orderedJson) as Map), + isA(), + ); + expect( + Timer.fromJson(jsonDecode(oldJson) as Map), + isA(), + ); + }); + }); +} From a8fa0ce4f5747390da75aecb5393ffc1f06d2b78 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Sat, 1 Apr 2023 08:54:05 +0200 Subject: [PATCH 09/11] unify url validation --- lib/src/blocs/login/login_bloc.dart | 2 + lib/src/screens/form/login_form.dart | 33 +------ lib/src/services/authentication_provider.dart | 30 +++---- lib/src/services/services.dart | 1 + lib/src/util/url_validator.dart | 85 +++++++++++++++++++ test/util/url_validator_test.dart | 38 +++++++++ 6 files changed, 141 insertions(+), 48 deletions(-) create mode 100644 lib/src/util/url_validator.dart create mode 100644 test/util/url_validator_test.dart diff --git a/lib/src/blocs/login/login_bloc.dart b/lib/src/blocs/login/login_bloc.dart index 992c9f6e..38873ff7 100644 --- a/lib/src/blocs/login/login_bloc.dart +++ b/lib/src/blocs/login/login_bloc.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; +import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart'; part 'login_event.dart'; part 'login_state.dart'; @@ -25,6 +26,7 @@ class LoginBloc extends Bloc { try { AppAuthentication appAuthentication; + assert(URLUtils.isSanitized(event.serverURL)); if (!event.isAppPassword) { appAuthentication = await userRepository.authenticate( diff --git a/lib/src/screens/form/login_form.dart b/lib/src/screens/form/login_form.dart index 95901011..d8a7a442 100644 --- a/lib/src/screens/form/login_form.dart +++ b/lib/src/screens/form/login_form.dart @@ -6,8 +6,8 @@ import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/login/login_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; +import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/checkbox_form_field.dart'; -import 'package:punycode/punycode.dart'; class LoginForm extends StatefulWidget { const LoginForm({super.key}); @@ -61,7 +61,7 @@ class _LoginFormState extends State with WidgetsBindingObserver { _formKey.currentState?.save(); if (_formKey.currentState?.validate() ?? false) { - final String serverUrl = _punyEncodeUrl(_serverUrl.text); + final String serverUrl = URLUtils.sanitizeUrl(_serverUrl.text); final String username = _username.text.trim(); final String password = _password.text.trim(); final String originalBasicAuth = 'Basic ${base64Encode( @@ -115,12 +115,8 @@ class _LoginFormState extends State with WidgetsBindingObserver { 'login.server_url.validator.empty', ); } - const urlPattern = - r"^(?:http(s)?:\/\/)?[\w.-]+(?:(?:\.[\w\.-]+)|(?:\:\d+))+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*$"; - final bool match = - RegExp(urlPattern, caseSensitive: false) - .hasMatch(_punyEncodeUrl(value)); - if (!match) { + + if (!URLUtils.isValid(value)) { return translate( 'login.server_url.validator.pattern', ); @@ -240,25 +236,4 @@ class _LoginFormState extends State with WidgetsBindingObserver { ), ); } - - String _punyEncodeUrl(String punycodeUrl) { - const String pattern = r"(?:\.|^)([^.]*?[^\x00-\x7F][^.]*?)(?:\.|$)"; - final RegExp expression = RegExp(pattern, caseSensitive: false); - String prefix = ""; - String url = punycodeUrl; - if (punycodeUrl.startsWith("https://")) { - url = punycodeUrl.replaceFirst("https://", ""); - prefix = "https://"; - } else if (punycodeUrl.startsWith("http://")) { - url = punycodeUrl.replaceFirst("http://", ""); - prefix = "http://"; - } - - while (expression.hasMatch(url)) { - final String match = expression.firstMatch(url)!.group(1)!; - url = url.replaceFirst(match, "xn--${punycodeEncode(match)}"); - } - - return prefix + url; - } } diff --git a/lib/src/services/authentication_provider.dart b/lib/src/services/authentication_provider.dart index 66a676e2..99b07afb 100644 --- a/lib/src/services/authentication_provider.dart +++ b/lib/src/services/authentication_provider.dart @@ -12,14 +12,9 @@ class AuthenticationProvider { required String originalBasicAuth, required bool isSelfSignedCertificate, }) async { - String url = serverUrl; - if (url.substring(0, 4) != 'http') { - url = 'https://$url'; - if (url.endsWith("/")) { - url = url.substring(0, url.length - 1); - } - } - final String urlInitialCall = '$url/ocs/v2.php/core/getapppassword'; + assert(URLUtils.isSanitized(serverUrl)); + + final String urlInitialCall = '$serverUrl/ocs/v2.php/core/getapppassword'; dio.Response response; try { @@ -49,12 +44,12 @@ class AuthenticationProvider { if (e.message?.contains("SocketException") ?? false) { throw translate( "login.errors.not_reachable", - args: {"server_url": url, "error_msg": e}, + args: {"server_url": serverUrl, "error_msg": e}, ); } else if (e.message?.contains("CERTIFICATE_VERIFY_FAILED") ?? false) { throw translate( "login.errors.certificate_failed", - args: {"server_url": url, "error_msg": e}, + args: {"server_url": serverUrl, "error_msg": e}, ); } throw translate("login.errors.request_failed", args: {"error_msg": e}); @@ -79,7 +74,7 @@ class AuthenticationProvider { 'Basic ${base64Encode(utf8.encode('$username:$appPassword'))}'; return AppAuthentication( - server: url, + server: serverUrl, loginName: username, basicAuth: basicAuth, isSelfSignedCertificate: isSelfSignedCertificate, @@ -103,15 +98,12 @@ class AuthenticationProvider { required String basicAuth, required bool isSelfSignedCertificate, }) async { - String url = serverUrl; - if (url.substring(0, 4) != 'http') { - url = 'https://$url'; - } + assert(URLUtils.isSanitized(serverUrl)); bool authenticated; try { authenticated = await checkAppAuthentication( - url, + serverUrl, basicAuth, isSelfSignedCertificate: isSelfSignedCertificate, ); @@ -119,12 +111,12 @@ class AuthenticationProvider { if (e.message?.contains("SocketException") ?? false) { throw translate( "login.errors.not_reachable", - args: {"server_url": url, "error_msg": e}, + args: {"server_url": serverUrl, "error_msg": e}, ); } else if (e.message?.contains("CERTIFICATE_VERIFY_FAILED") ?? false) { throw translate( "login.errors.certificate_failed", - args: {"server_url": url, "error_msg": e}, + args: {"server_url": serverUrl, "error_msg": e}, ); } throw translate("login.errors.request_failed", args: {"error_msg": e}); @@ -132,7 +124,7 @@ class AuthenticationProvider { if (authenticated) { return AppAuthentication( - server: url, + server: serverUrl, loginName: username, basicAuth: basicAuth, isSelfSignedCertificate: isSelfSignedCertificate, diff --git a/lib/src/services/services.dart b/lib/src/services/services.dart index a13f4aae..50cf5a17 100644 --- a/lib/src/services/services.dart +++ b/lib/src/services/services.dart @@ -19,6 +19,7 @@ import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe_import_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/util/custom_cache_manager.dart'; +import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart'; import 'package:timezone/data/latest_10y.dart' as tz; import 'package:timezone/timezone.dart' as tz; import 'package:xml/xml.dart'; diff --git a/lib/src/util/url_validator.dart b/lib/src/util/url_validator.dart new file mode 100644 index 00000000..bbcaf8e3 --- /dev/null +++ b/lib/src/util/url_validator.dart @@ -0,0 +1,85 @@ +import 'package:punycode/punycode.dart'; + +/// Utilities for validating and eycaping urls +class URLUtils { + const URLUtils._(); // coverage:ignore-line + + /// Validates a given [url]. + /// + /// Punycode urls are encoded before checking. + /// Example: + /// ```dart + /// print(URLUtils.isValid('http://foo.bar'); // true + /// print(URLUtils.isValid('foo.bar'); // true + /// print(URLUtils.isValid('https://öüäööß.foo.bar/'); // true + /// print(URLUtils.isValid('https://foo/bar'); // false + /// print(URLUtils.isValid(''); // false + /// ``` + static bool isValid(String url) { + const urlPattern = + r"^(?:http(s)?:\/\/)?[\w.-]+(?:(?:\.[\w\.-]+)|(?:\:\d+))+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*$"; + return RegExp(urlPattern, caseSensitive: false) + .hasMatch(_punyEncodeUrl(url)); + } + + /// Punycode encodes an entire [url]. + static String _punyEncodeUrl(String url) { + String prefix = ""; + String punycodeUrl = url; + if (url.startsWith("https://")) { + punycodeUrl = url.replaceFirst("https://", ""); + prefix = "https://"; + } else if (url.startsWith("http://")) { + punycodeUrl = url.replaceFirst("http://", ""); + prefix = "http://"; + } + + const String pattern = r"(?:\.|^)([^.]*?[^\x00-\x7F][^.]*?)(?:\.|$)"; + final RegExp expression = RegExp(pattern, caseSensitive: false); + + final matches = expression.allMatches(punycodeUrl); + for (final exp in matches) { + final String match = exp.group(1)!; + + punycodeUrl = + punycodeUrl.replaceFirst(match, "xn--${punycodeEncode(match)}"); + } + + return prefix + punycodeUrl; + } + + /// Checks if the url has been sanitized + static bool isSanitized(String url) => url == sanitizeUrl(url); + + /// Sanitizes a given [url]. + /// + /// Strips trailing `/` and guesses the protocol to be `https` when not specified. + /// Throws a `FormatException` when the url cannot be validated with [URLUtils.isValid]. + /// + /// Example: + /// ```dart + /// print(URLUtils.sanitizeUrl('http://foo.bar'); // http://foo.bar + /// print(URLUtils.sanitizeUrl('http://foo.bar/'); // http://foo.bar + /// print(URLUtils.sanitizeUrl('https://foo.bar'); // https://foo.bar + /// print(URLUtils.sanitizeUrl('foo.bar'); // https://foo.bar + /// print(URLUtils.sanitizeUrl('foo.bar/cloud/'); // https://foo.bar/cloud + /// print(URLUtils.sanitizeUrl(''); // FormatException + /// ``` + static String sanitizeUrl(String url) { + if (!isValid(url)) { + throw const FormatException( + "given url is not valid. Please validate first with URLUtils.isValid(url)", + ); + } + + String encodedUrl = _punyEncodeUrl(url); + if (encodedUrl.substring(0, 4) != 'http') { + encodedUrl = 'https://$encodedUrl'; + } + if (encodedUrl.endsWith("/")) { + encodedUrl = encodedUrl.substring(0, encodedUrl.length - 1); + } + + return encodedUrl; + } +} diff --git a/test/util/url_validator_test.dart b/test/util/url_validator_test.dart new file mode 100644 index 00000000..c52fbc94 --- /dev/null +++ b/test/util/url_validator_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart'; + +void main() { + group('URLUtils', () { + test('.isValid() validates a url', () { + const emptyUrl = ''; + expect(URLUtils.isValid(emptyUrl), false); + const validUrl = 'http://foo.bar'; + expect(URLUtils.isValid(validUrl), true); + const noProtocol = 'foo.bar'; + expect(URLUtils.isValid(noProtocol), true); + const punycodeUrl = 'https://öüäööß.foo.bar/'; + expect(URLUtils.isValid(punycodeUrl), true); + const missingTLD = 'https://foo/bar'; + expect(URLUtils.isValid(missingTLD), false); + }); + + test('.isSanitized() check sanitized', () { + const dirtyUrl = 'foo.bar/cloud/'; + expect(URLUtils.isSanitized(dirtyUrl), false); + + const cleanUrl = 'http://foo.bar'; + expect(URLUtils.isSanitized(cleanUrl), true); + }); + + test('.sanitizeUrl() sanitizes URL', () { + const insecureUrl = 'http://foo.bar'; + expect(URLUtils.sanitizeUrl(insecureUrl), equals('http://foo.bar')); + const secureUrl = 'https://foo.bar/'; + expect(URLUtils.sanitizeUrl(secureUrl), equals('https://foo.bar')); + const plainDomain = 'foo.bar/'; + expect(URLUtils.sanitizeUrl(plainDomain), equals('https://foo.bar')); + const subdirUrl = 'foo.bar/cloud/'; + expect(URLUtils.sanitizeUrl(subdirUrl), equals('https://foo.bar/cloud')); + }); + }); +} From 1914a540a208811d6d2104521f2a33e92990faba Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Sat, 1 Apr 2023 11:24:13 +0200 Subject: [PATCH 10/11] unify AppAuthentication parsing also adding tests --- lib/src/models/app_authentication.dart | 90 ++++++++++++------- lib/src/models/app_authentication.g.dart | 23 +++++ lib/src/screens/form/login_form.dart | 11 +-- lib/src/services/authentication_provider.dart | 7 +- test/models/app_authentication_test.dart | 62 +++++++++++++ 5 files changed, 151 insertions(+), 42 deletions(-) create mode 100644 lib/src/models/app_authentication.g.dart create mode 100644 test/models/app_authentication_test.dart diff --git a/lib/src/models/app_authentication.dart b/lib/src/models/app_authentication.dart index 617809c8..9f67350e 100644 --- a/lib/src/models/app_authentication.dart +++ b/lib/src/models/app_authentication.dart @@ -1,10 +1,14 @@ import 'dart:convert'; -import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:nextcloud_cookbook_flutter/src/util/self_signed_certificate_http_overrides.dart'; +import 'package:dio/io.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; -class AppAuthentication { +part 'app_authentication.g.dart'; + +@JsonSerializable() +class AppAuthentication extends Equatable { final String server; final String loginName; final String basicAuth; @@ -18,47 +22,71 @@ class AppAuthentication { required this.basicAuth, required this.isSelfSignedCertificate, }) { - authenticatedClient.options.headers["authorization"] = basicAuth; - authenticatedClient.options.headers["User-Agent"] = "Cookbook App"; - authenticatedClient.options.responseType = ResponseType.plain; + authenticatedClient.options + ..headers["authorization"] = basicAuth + ..headers["User-Agent"] = "Cookbook App" + ..responseType = ResponseType.plain; if (isSelfSignedCertificate) { - HttpOverrides.global = SelfSignedCertificateHttpOverride(); + authenticatedClient.httpClientAdapter = IOHttpClientAdapter( + onHttpClientCreate: (client) { + client.badCertificateCallback = (cert, host, port) => true; + return client; + }, + ); } } - factory AppAuthentication.fromJson(String jsonString) { - final jsonData = json.decode(jsonString) as Map; + factory AppAuthentication.fromJsonString(String jsonString) => + AppAuthentication.fromJson( + json.decode(jsonString) as Map, + ); - final basicAuth = jsonData.containsKey("basicAuth") - ? jsonData['basicAuth'] as String - : 'Basic ${base64Encode( - utf8.encode( - '${jsonData["loginName"]}:${jsonData["appPassword"]}', - ), - )}'; + factory AppAuthentication.fromJson(Map jsonData) { + try { + return _$AppAuthenticationFromJson(jsonData); + // ignore: avoid_catching_errors + } on TypeError { + final basicAuth = parseBasicAuth( + jsonData["loginName"] as String, + jsonData["appPassword"] as String, + ); - final selfSignedCertificate = - jsonData['isSelfSignedCertificate'] as bool? ?? false; + final selfSignedCertificate = + jsonData['isSelfSignedCertificate'] as bool? ?? false; - return AppAuthentication( - server: jsonData["server"] as String, - loginName: jsonData["loginName"] as String, - basicAuth: basicAuth, - isSelfSignedCertificate: selfSignedCertificate, - ); + return AppAuthentication( + server: jsonData["server"] as String, + loginName: jsonData["loginName"] as String, + basicAuth: basicAuth, + isSelfSignedCertificate: selfSignedCertificate, + ); + } } - String toJson() { - return json.encode({ - "server": server, - "loginName": loginName, - "basicAuth": basicAuth, - "isSelfSignedCertificate": isSelfSignedCertificate, - }); + String toJsonString() => json.encode(toJson()); + Map toJson() => _$AppAuthenticationToJson(this); + + String get password { + final base64 = basicAuth.substring(6); + final string = utf8.decode(base64Decode(base64)); + final auth = string.split(":"); + return auth[1]; + } + + static String parseBasicAuth(String loginName, String appPassword) { + return 'Basic ${base64Encode(utf8.encode('$loginName:$appPassword'))}'; } @override String toString() => 'LoggedIn { token: $server, $loginName, isSelfSignedCertificate $isSelfSignedCertificate}'; + + @override + List get props => [ + server, + loginName, + basicAuth, + isSelfSignedCertificate, + ]; } diff --git a/lib/src/models/app_authentication.g.dart b/lib/src/models/app_authentication.g.dart new file mode 100644 index 00000000..2edf4cbd --- /dev/null +++ b/lib/src/models/app_authentication.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_authentication.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AppAuthentication _$AppAuthenticationFromJson(Map json) => + AppAuthentication( + server: json['server'] as String, + loginName: json['loginName'] as String, + basicAuth: json['basicAuth'] as String, + isSelfSignedCertificate: json['isSelfSignedCertificate'] as bool, + ); + +Map _$AppAuthenticationToJson(AppAuthentication instance) => + { + 'server': instance.server, + 'loginName': instance.loginName, + 'basicAuth': instance.basicAuth, + 'isSelfSignedCertificate': instance.isSelfSignedCertificate, + }; diff --git a/lib/src/screens/form/login_form.dart b/lib/src/screens/form/login_form.dart index d8a7a442..437cd853 100644 --- a/lib/src/screens/form/login_form.dart +++ b/lib/src/screens/form/login_form.dart @@ -1,10 +1,9 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/login/login_bloc.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/checkbox_form_field.dart'; @@ -64,11 +63,9 @@ class _LoginFormState extends State with WidgetsBindingObserver { final String serverUrl = URLUtils.sanitizeUrl(_serverUrl.text); final String username = _username.text.trim(); final String password = _password.text.trim(); - final String originalBasicAuth = 'Basic ${base64Encode( - utf8.encode( - '$username:$password', - ), - )}'; + final String originalBasicAuth = + AppAuthentication.parseBasicAuth(username, password); + BlocProvider.of(context).add( LoginButtonPressed( serverURL: serverUrl, diff --git a/lib/src/services/authentication_provider.dart b/lib/src/services/authentication_provider.dart index 99b07afb..6651d661 100644 --- a/lib/src/services/authentication_provider.dart +++ b/lib/src/services/authentication_provider.dart @@ -70,8 +70,7 @@ class AuthenticationProvider { throw translate("login.errors.parse_missing", args: {"error_msg": e}); } - final String basicAuth = - 'Basic ${base64Encode(utf8.encode('$username:$appPassword'))}'; + final basicAuth = AppAuthentication.parseBasicAuth(username, appPassword); return AppAuthentication( server: serverUrl, @@ -156,7 +155,7 @@ class AuthenticationProvider { throw translate('login.errors.authentication_not_found'); } else { currentAppAuthentication = - AppAuthentication.fromJson(appAuthenticationString); + AppAuthentication.fromJsonString(appAuthenticationString); } } @@ -216,7 +215,7 @@ class AuthenticationProvider { currentAppAuthentication = appAuthentication; await _secureStorage.write( key: _appAuthenticationKey, - value: appAuthentication.toJson(), + value: appAuthentication.toJsonString(), ); } diff --git a/test/models/app_authentication_test.dart b/test/models/app_authentication_test.dart new file mode 100644 index 00000000..da8f10d4 --- /dev/null +++ b/test/models/app_authentication_test.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; + +void main() { + const String server = 'https://example.com'; + const String loginName = 'admin'; + const String password = 'password'; + final String basicAuth = 'Basic ${base64Encode( + utf8.encode( + '$loginName:$password', + ), + )}'; + const bool isSelfSignedCertificate = false; + + final auth = AppAuthentication( + server: server, + loginName: loginName, + basicAuth: basicAuth, + isSelfSignedCertificate: isSelfSignedCertificate, + ); + + final encodedJson = + '"{\\"server\\":\\"$server\\",\\"loginName\\":\\"$loginName\\",\\"basicAuth\\":\\"$basicAuth\\",\\"isSelfSignedCertificate\\":$isSelfSignedCertificate}"'; + final jsonBasicAuth = + '{"server":"$server","loginName":"$loginName","basicAuth":"$basicAuth","isSelfSignedCertificate":$isSelfSignedCertificate}'; + const jsonPassword = + '{"server":"$server","loginName":"$loginName","appPassword":"$password","isSelfSignedCertificate":$isSelfSignedCertificate}'; + + group(AppAuthentication, () { + test("toJson", () { + expect(jsonEncode(auth.toJsonString()), equals(encodedJson)); + }); + + test("fromJson", () { + expect( + AppAuthentication.fromJsonString(jsonBasicAuth), + equals(auth), + ); + expect( + AppAuthentication.fromJsonString(jsonPassword), + equals(auth), + ); + }); + + test("password", () { + expect(auth.password, equals(password)); + }); + + test("parseBasicAuth", () { + expect( + AppAuthentication.parseBasicAuth(loginName, password), + equals(basicAuth), + ); + }); + + test("toJson does not contain password", () { + expect(auth.toString(), isNot(contains(basicAuth))); + }); + }); +} From a86a5ae3f7f77a5c3f64b479b9ee57fa3951379c Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Sat, 1 Apr 2023 12:31:35 +0200 Subject: [PATCH 11/11] refactor data flow --- lib/src/screens/category/category_screen.dart | 3 +- lib/src/screens/form/recipe_form.dart | 78 +++++++++---------- lib/src/screens/recipe_create_screen.dart | 15 +--- lib/src/screens/recipe_edit_screen.dart | 9 +-- 4 files changed, 43 insertions(+), 62 deletions(-) diff --git a/lib/src/screens/category/category_screen.dart b/lib/src/screens/category/category_screen.dart index b2079d38..22d7d5ce 100644 --- a/lib/src/screens/category/category_screen.dart +++ b/lib/src/screens/category/category_screen.dart @@ -7,7 +7,6 @@ import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authenticati import 'package:nextcloud_cookbook_flutter/src/blocs/categories/categories_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/my_settings_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe/recipe_screen.dart'; @@ -41,7 +40,7 @@ class _CategoryScreenState extends State { context, MaterialPageRoute( builder: (context) { - return RecipeCreateScreen(Recipe.empty()); + return const RecipeCreateScreen(); }, ), ); diff --git a/lib/src/screens/form/recipe_form.dart b/lib/src/screens/form/recipe_form.dart index c868c51e..1ad7576c 100644 --- a/lib/src/screens/form/recipe_form.dart +++ b/lib/src/screens/form/recipe_form.dart @@ -10,21 +10,12 @@ import 'package:nextcloud_cookbook_flutter/src/widget/input/duration_form_field. import 'package:nextcloud_cookbook_flutter/src/widget/input/integer_text_form_field.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/input/reorderable_list_form_field.dart'; -typedef RecipeFormSubmit = void Function( - MutableRecipe mutableRecipe, - BuildContext context, -); - class RecipeForm extends StatefulWidget { - final Recipe recipe; - final String buttonSubmitText; - final RecipeFormSubmit recipeFormSubmit; + final Recipe? recipe; - const RecipeForm( - this.recipe, - this.buttonSubmitText, - this.recipeFormSubmit, { + const RecipeForm({ super.key, + this.recipe, }); @override @@ -33,15 +24,18 @@ class RecipeForm extends StatefulWidget { class _RecipeFormState extends State { final _formKey = GlobalKey(); - late Recipe recipe; late MutableRecipe _mutableRecipe; late TextEditingController categoryController; @override void initState() { - recipe = widget.recipe; - _mutableRecipe = recipe.toMutableRecipe(); - categoryController = TextEditingController(text: recipe.recipeCategory); + _mutableRecipe = Recipe.empty().toMutableRecipe(); + + if (widget.recipe != null) { + _mutableRecipe = widget.recipe!.toMutableRecipe(); + } + categoryController = + TextEditingController(text: _mutableRecipe.recipeCategory); super.initState(); } @@ -71,7 +65,7 @@ class _RecipeFormState extends State { ), TextFormField( enabled: enabled, - initialValue: recipe.name, + initialValue: _mutableRecipe.name, onChanged: (value) { _mutableRecipe.name = value; }, @@ -90,7 +84,7 @@ class _RecipeFormState extends State { ), TextFormField( enabled: enabled, - initialValue: recipe.description, + initialValue: _mutableRecipe.description, maxLines: 100, minLines: 1, onChanged: (value) { @@ -116,16 +110,12 @@ class _RecipeFormState extends State { ), suggestionsCallback: DataRepository().getMatchingCategoryNames, - itemBuilder: (context, String? suggestion) { - if (suggestion != null) { - return ListTile( - title: Text(suggestion), - ); - } - return const SizedBox(); + itemBuilder: (context, suggestion) { + return ListTile( + title: Text(suggestion), + ); }, - onSuggestionSelected: (String? suggestion) { - if (suggestion == null) return; + onSuggestionSelected: (String suggestion) { categoryController.text = suggestion; }, onSaved: (value) { @@ -147,7 +137,7 @@ class _RecipeFormState extends State { ), TextFormField( enabled: enabled, - initialValue: recipe.keywords, + initialValue: _mutableRecipe.keywords, onChanged: (value) { _mutableRecipe.keywords = value; }, @@ -166,7 +156,7 @@ class _RecipeFormState extends State { ), TextFormField( enabled: enabled, - initialValue: recipe.url, + initialValue: _mutableRecipe.url, onChanged: (value) { _mutableRecipe.url = value; }, @@ -186,7 +176,7 @@ class _RecipeFormState extends State { TextFormField( enabled: false, style: const TextStyle(color: Colors.grey), - initialValue: recipe.imageUrl, + initialValue: _mutableRecipe.imageUrl, onChanged: (value) { _mutableRecipe.imageUrl = value; }, @@ -205,7 +195,7 @@ class _RecipeFormState extends State { ), IntegerTextFormField( enabled: enabled, - initialValue: recipe.recipeYield, + initialValue: _mutableRecipe.recipeYield, onChanged: (value) => _mutableRecipe.recipeYield = value, onSaved: (value) => _mutableRecipe.recipeYield = value, @@ -215,37 +205,37 @@ class _RecipeFormState extends State { DurationFormField( title: translate('recipe.fields.time.prep'), state: state, - duration: recipe.prepTime, + duration: _mutableRecipe.prepTime, onChanged: (value) => {_mutableRecipe.prepTime = value}, ), DurationFormField( title: translate('recipe.fields.time.cook'), state: state, - duration: recipe.cookTime, + duration: _mutableRecipe.cookTime, onChanged: (value) => {_mutableRecipe.cookTime = value}, ), DurationFormField( title: translate('recipe.fields.time.total'), state: state, - duration: recipe.totalTime, + duration: _mutableRecipe.totalTime, onChanged: (value) => {_mutableRecipe.totalTime = value}, ), ReorderableListFormField( title: translate('recipe.fields.tools'), - items: recipe.tool, + items: _mutableRecipe.tool, state: state, onSave: (value) => {_mutableRecipe.tool = value}, ), ReorderableListFormField( title: translate('recipe.fields.ingredients'), - items: recipe.recipeIngredient, + items: _mutableRecipe.recipeIngredient, state: state, onSave: (value) => {_mutableRecipe.recipeIngredient = value}, ), ReorderableListFormField( title: translate('recipe.fields.instructions'), - items: recipe.recipeInstructions, + items: _mutableRecipe.recipeInstructions, state: state, onSave: (value) => {_mutableRecipe.recipeInstructions = value}, @@ -256,7 +246,13 @@ class _RecipeFormState extends State { onPressed: () { if (_formKey.currentState?.validate() ?? false) { _formKey.currentState?.save(); - widget.recipeFormSubmit(_mutableRecipe, context); + if (widget.recipe == null) { + BlocProvider.of(context) + .add(RecipeCreated(_mutableRecipe.toRecipe())); + } else { + BlocProvider.of(context) + .add(RecipeUpdated(_mutableRecipe.toRecipe())); + } } }, child: () { @@ -272,7 +268,11 @@ class _RecipeFormState extends State { case RecipeStatus.createSuccess: case RecipeStatus.createFailure: default: - return Text(widget.buttonSubmitText); + return Text( + widget.recipe == null + ? translate('recipe_create.button') + : translate('recipe_edit.button'), + ); } }(), ), diff --git a/lib/src/screens/recipe_create_screen.dart b/lib/src/screens/recipe_create_screen.dart index 0d81d759..81209f63 100644 --- a/lib/src/screens/recipe_create_screen.dart +++ b/lib/src/screens/recipe_create_screen.dart @@ -2,16 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/form/recipe_form.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe/recipe_screen.dart'; class RecipeCreateScreen extends StatelessWidget { - final Recipe recipe; - - const RecipeCreateScreen( - this.recipe, { + const RecipeCreateScreen({ super.key, }); @@ -48,14 +44,7 @@ class RecipeCreateScreen extends StatelessWidget { child: Text(translate('recipe_create.title')), ), ), - body: RecipeForm( - recipe, - translate('recipe_create.button'), - (mutableRecipe, context) => { - BlocProvider.of(context) - .add(RecipeCreated(mutableRecipe.toRecipe())) - }, - ), + body: const RecipeForm(), ), ); } diff --git a/lib/src/screens/recipe_edit_screen.dart b/lib/src/screens/recipe_edit_screen.dart index f71ba639..b6159011 100644 --- a/lib/src/screens/recipe_edit_screen.dart +++ b/lib/src/screens/recipe_edit_screen.dart @@ -48,14 +48,7 @@ class RecipeEditScreen extends StatelessWidget { child: Text(translate('recipe_edit.title')), ), ), - body: RecipeForm( - recipe, - translate('recipe_edit.button'), - (mutableRecipe, context) => { - BlocProvider.of(context) - .add(RecipeUpdated(mutableRecipe.toRecipe())) - }, - ), + body: RecipeForm(recipe: recipe), ), ); }