diff --git a/analysis_options.yaml b/analysis_options.yaml index 83448ad4..fd0a0cd0 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -5,3 +5,24 @@ include: package:lint/strict.yaml linter: rules: sort_pub_dependencies: false + prefer_final_parameters: false + prefer_asserts_with_message: false + only_throw_errors: false + + prefer_mixin: true + discarded_futures: true + unawaited_futures: true + prefer_expression_function_bodies: true + always_put_control_body_on_new_line: true + use_key_in_widget_constructors: true + always_put_required_named_parameters_first: true + prefer_single_quotes: true + sort_constructors_first: true + omit_local_variable_types: true + prefer_int_literals: true + cascade_invocations: true + avoid_equals_and_hash_code_on_mutable_classes: true + avoid_types_on_closure_parameters: true + use_decorated_box: true + unnecessary_lambdas: true + prefer_foreach: true diff --git a/lib/main.dart b/lib/main.dart index 865a05a0..e9ae2277 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -42,19 +44,13 @@ void main() async { MultiBlocProvider( providers: [ BlocProvider( - create: (context) { - return AuthenticationBloc()..add(const AppStarted()); - }, + create: (context) => AuthenticationBloc()..add(const AppStarted()), ), BlocProvider( - create: (context) { - return RecipesShortBloc(); - }, + create: (context) => RecipesShortBloc(), ), BlocProvider( - create: (context) { - return CategoriesBloc(); - }, + create: (context) => CategoriesBloc(), ) ], child: const App(), @@ -87,51 +83,49 @@ class _AppState extends State { final savedLocalization = Settings.getValue( SettingKeys.language.name, ); - changeLocale(context, savedLocalization); + unawaited(changeLocale(context, savedLocalization)); } @override - Widget build(BuildContext context) { - return ThemeModeHandler( - manager: ThemeModeManager(), - builder: (ThemeMode themeMode) => MaterialApp( - navigatorKey: IntentRepository().getNavigationKey(), - themeMode: themeMode, - theme: AppTheme.lightThemeData, - darkTheme: AppTheme.darkThemeData, - home: BlocConsumer( - builder: (context, state) { - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( - systemNavigationBarColor: - Theme.of(context).scaffoldBackgroundColor, - ), - ); + Widget build(BuildContext context) => ThemeModeHandler( + manager: ThemeModeManager(), + builder: (themeMode) => MaterialApp( + navigatorKey: IntentRepository().getNavigationKey(), + themeMode: themeMode, + theme: AppTheme.lightThemeData, + darkTheme: AppTheme.darkThemeData, + home: BlocConsumer( + builder: (context, state) { + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + systemNavigationBarColor: + Theme.of(context).scaffoldBackgroundColor, + ), + ); - switch (state.status) { - case AuthenticationStatus.loading: - return const SplashPage(); - case AuthenticationStatus.authenticated: - return const CategoryScreen(); - case AuthenticationStatus.unauthenticated: - return const LoginScreen(); - case AuthenticationStatus.invalid: - return const LoginScreen( - invalidCredentials: true, - ); - case AuthenticationStatus.error: - return LoadingErrorScreen(message: state.error!); - } - }, - listener: (context, state) async { - if (state.status != AuthenticationStatus.loading) { - FlutterNativeSplash.remove(); - } else if (state.status == AuthenticationStatus.authenticated) { - await IntentRepository().handleIntent(); - } - }, + switch (state.status) { + case AuthenticationStatus.loading: + return const SplashPage(); + case AuthenticationStatus.authenticated: + return const CategoryScreen(); + case AuthenticationStatus.unauthenticated: + return const LoginScreen(); + case AuthenticationStatus.invalid: + return const LoginScreen( + invalidCredentials: true, + ); + case AuthenticationStatus.error: + return LoadingErrorScreen(message: state.error!); + } + }, + listener: (context, state) async { + if (state.status != AuthenticationStatus.loading) { + FlutterNativeSplash.remove(); + } else if (state.status == AuthenticationStatus.authenticated) { + await IntentRepository().handleIntent(); + } + }, + ), ), - ), - ); - } + ); } diff --git a/lib/src/blocs/authentication/authentication_bloc.dart b/lib/src/blocs/authentication/authentication_bloc.dart index 0343ff78..a7346b01 100644 --- a/lib/src/blocs/authentication/authentication_bloc.dart +++ b/lib/src/blocs/authentication/authentication_bloc.dart @@ -8,19 +8,18 @@ part 'authentication_state.dart'; class AuthenticationBloc extends Bloc { - final UserRepository userRepository = UserRepository(); - AuthenticationBloc() : super(AuthenticationState()) { on(_mapAppStartedEventToState); on(_mapLoggedInEventToState); on(_mapLoggedOutEventToState); } + final UserRepository userRepository = UserRepository(); Future _mapAppStartedEventToState( AppStarted event, Emitter emit, ) async { - final bool hasToken = await userRepository.hasAppAuthentication(); + final hasToken = await userRepository.hasAppAuthentication(); if (hasToken) { await userRepository.loadAppAuthentication(); diff --git a/lib/src/blocs/authentication/authentication_event.dart b/lib/src/blocs/authentication/authentication_event.dart index 47634c4d..dbefb8cf 100644 --- a/lib/src/blocs/authentication/authentication_event.dart +++ b/lib/src/blocs/authentication/authentication_event.dart @@ -12,9 +12,8 @@ class AppStarted extends AuthenticationEvent { } class LoggedIn extends AuthenticationEvent { - final AppAuthentication appAuthentication; - const LoggedIn({required this.appAuthentication}); + final AppAuthentication appAuthentication; @override List get props => [appAuthentication]; diff --git a/lib/src/blocs/authentication/authentication_state.dart b/lib/src/blocs/authentication/authentication_state.dart index 4e02dd59..d1bfa5e1 100644 --- a/lib/src/blocs/authentication/authentication_state.dart +++ b/lib/src/blocs/authentication/authentication_state.dart @@ -22,9 +22,6 @@ enum AuthenticationStatus { } class AuthenticationState extends Equatable { - final AuthenticationStatus status; - final String? error; - const AuthenticationState({ this.status = AuthenticationStatus.loading, this.error, @@ -32,6 +29,8 @@ class AuthenticationState extends Equatable { (status != AuthenticationStatus.error && error == null) || (status == AuthenticationStatus.error && error != null), ); + final AuthenticationStatus status; + final String? error; @override List get props => [status, error]; diff --git a/lib/src/blocs/categories/categories_bloc.dart b/lib/src/blocs/categories/categories_bloc.dart index 0ae589cc..13bc75b5 100644 --- a/lib/src/blocs/categories/categories_bloc.dart +++ b/lib/src/blocs/categories/categories_bloc.dart @@ -7,11 +7,10 @@ part 'categories_event.dart'; part 'categories_state.dart'; class CategoriesBloc extends Bloc { - final DataRepository dataRepository = DataRepository(); - CategoriesBloc() : super(CategoriesState()) { on(_mapCategoriesLoadedEventToState); } + final DataRepository dataRepository = DataRepository(); Future _mapCategoriesLoadedEventToState( CategoriesLoaded event, diff --git a/lib/src/blocs/categories/categories_state.dart b/lib/src/blocs/categories/categories_state.dart index 8ebf1bff..d01fed11 100644 --- a/lib/src/blocs/categories/categories_state.dart +++ b/lib/src/blocs/categories/categories_state.dart @@ -8,11 +8,6 @@ enum CategoriesStatus { } class CategoriesState extends Equatable { - final CategoriesStatus status; - final String? error; - final Iterable? categories; - final Iterable? recipes; - CategoriesState({ this.status = CategoriesStatus.loadInProgress, this.error, @@ -33,6 +28,10 @@ class CategoriesState extends Equatable { assert(error != null && categories == null && recipes == null); } } + final CategoriesStatus status; + final String? error; + final Iterable? categories; + final Iterable? recipes; @override List get props => [status, error, categories]; diff --git a/lib/src/blocs/login/login_bloc.dart b/lib/src/blocs/login/login_bloc.dart index 38873ff7..eb64c913 100644 --- a/lib/src/blocs/login/login_bloc.dart +++ b/lib/src/blocs/login/login_bloc.dart @@ -9,14 +9,13 @@ part 'login_event.dart'; part 'login_state.dart'; class LoginBloc extends Bloc { - final UserRepository userRepository = UserRepository(); - final AuthenticationBloc authenticationBloc; - LoginBloc({ required this.authenticationBloc, }) : super(LoginState()) { on(_mapLoginButtonPressedEventToState); } + final UserRepository userRepository = UserRepository(); + final AuthenticationBloc authenticationBloc; Future _mapLoginButtonPressedEventToState( LoginButtonPressed event, diff --git a/lib/src/blocs/login/login_event.dart b/lib/src/blocs/login/login_event.dart index 4f8fa4dc..6d3db364 100644 --- a/lib/src/blocs/login/login_event.dart +++ b/lib/src/blocs/login/login_event.dart @@ -8,12 +8,6 @@ abstract class LoginEvent extends Equatable { } class LoginButtonPressed extends LoginEvent { - final String serverURL; - final String username; - final String originalBasicAuth; - final bool isAppPassword; - final bool isSelfSignedCertificate; - const LoginButtonPressed({ required this.serverURL, required this.username, @@ -21,6 +15,11 @@ class LoginButtonPressed extends LoginEvent { required this.isAppPassword, required this.isSelfSignedCertificate, }); + final String serverURL; + final String username; + final String originalBasicAuth; + final bool isAppPassword; + final bool isSelfSignedCertificate; @override List get props => diff --git a/lib/src/blocs/login/login_state.dart b/lib/src/blocs/login/login_state.dart index 535026c8..baf57287 100644 --- a/lib/src/blocs/login/login_state.dart +++ b/lib/src/blocs/login/login_state.dart @@ -7,9 +7,6 @@ enum LoginStatus { } class LoginState extends Equatable { - final LoginStatus status; - final String? error; - const LoginState({ this.status = LoginStatus.initial, this.error, @@ -17,6 +14,8 @@ class LoginState extends Equatable { (status != LoginStatus.failure && error == null) || (status == LoginStatus.failure && error != null), ); + final LoginStatus status; + final String? error; @override List get props => [status, error]; diff --git a/lib/src/blocs/recipe/recipe_bloc.dart b/lib/src/blocs/recipe/recipe_bloc.dart index 63df7aab..26e5df03 100644 --- a/lib/src/blocs/recipe/recipe_bloc.dart +++ b/lib/src/blocs/recipe/recipe_bloc.dart @@ -7,8 +7,6 @@ part 'recipe_event.dart'; part 'recipe_state.dart'; class RecipeBloc extends Bloc { - final DataRepository dataRepository = DataRepository(); - RecipeBloc() : super(RecipeState()) { on(_mapRecipeLoadedToState); on(_mapRecipeUpdatedToState); @@ -16,6 +14,7 @@ class RecipeBloc extends Bloc { on(_mapRecipeCreatedToState); on(_mapRecipeDeletedToState); } + final DataRepository dataRepository = DataRepository(); Future _mapRecipeLoadedToState( RecipeLoaded recipeLoaded, diff --git a/lib/src/blocs/recipe/recipe_event.dart b/lib/src/blocs/recipe/recipe_event.dart index 4a6c4260..273c6deb 100644 --- a/lib/src/blocs/recipe/recipe_event.dart +++ b/lib/src/blocs/recipe/recipe_event.dart @@ -8,45 +8,40 @@ abstract class RecipeEvent extends Equatable { } class RecipeLoaded extends RecipeEvent { - final String recipeId; - const RecipeLoaded(this.recipeId); + final String recipeId; @override List get props => [recipeId]; } class RecipeUpdated extends RecipeEvent { - final Recipe recipe; - const RecipeUpdated(this.recipe); + final Recipe recipe; @override List get props => [recipe]; } class RecipeCreated extends RecipeEvent { - final Recipe recipe; - const RecipeCreated(this.recipe); + final Recipe recipe; @override List get props => [recipe]; } class RecipeImported extends RecipeEvent { - final String url; - const RecipeImported(this.url); + final String url; @override List get props => [url]; } class RecipeDeleted extends RecipeEvent { - final Recipe recipe; - const RecipeDeleted(this.recipe); + final Recipe recipe; @override List get props => [recipe]; diff --git a/lib/src/blocs/recipe/recipe_state.dart b/lib/src/blocs/recipe/recipe_state.dart index 45f77027..0fbba9fd 100644 --- a/lib/src/blocs/recipe/recipe_state.dart +++ b/lib/src/blocs/recipe/recipe_state.dart @@ -19,11 +19,6 @@ enum RecipeStatus { } class RecipeState extends Equatable { - final RecipeStatus status; - final String? error; - final Recipe? recipe; - final String? recipeId; - RecipeState({ this.status = RecipeStatus.loadInProgress, this.error, @@ -55,6 +50,10 @@ class RecipeState extends Equatable { assert(error != null && recipe == null && recipeId == null); } } + final RecipeStatus status; + final String? error; + final Recipe? recipe; + final String? recipeId; @override List get props => [status, error, recipe, recipeId]; diff --git a/lib/src/blocs/recipes_short/recipes_short_bloc.dart b/lib/src/blocs/recipes_short/recipes_short_bloc.dart index 45b17f3f..9bea186f 100644 --- a/lib/src/blocs/recipes_short/recipes_short_bloc.dart +++ b/lib/src/blocs/recipes_short/recipes_short_bloc.dart @@ -7,12 +7,11 @@ part 'recipes_short_event.dart'; part 'recipes_short_state.dart'; class RecipesShortBloc extends Bloc { - final DataRepository dataRepository = DataRepository(); - RecipesShortBloc() : super(RecipesShortState()) { on(_mapRecipesShortLoadedToState); on(_mapRecipesShortLoadedAllToState); } + final DataRepository dataRepository = DataRepository(); Future _mapRecipesShortLoadedToState( RecipesShortLoaded recipesShortLoaded, @@ -32,7 +31,7 @@ class RecipesShortBloc extends Bloc { emit( RecipesShortState( status: RecipesShortStatus.loadFailure, - error: "", + error: '', ), ); } @@ -59,7 +58,7 @@ class RecipesShortBloc extends Bloc { emit( RecipesShortState( status: RecipesShortStatus.loadAllFailure, - error: "", + error: '', ), ); } diff --git a/lib/src/blocs/recipes_short/recipes_short_event.dart b/lib/src/blocs/recipes_short/recipes_short_event.dart index c7a35f61..74e95378 100644 --- a/lib/src/blocs/recipes_short/recipes_short_event.dart +++ b/lib/src/blocs/recipes_short/recipes_short_event.dart @@ -8,9 +8,8 @@ abstract class RecipesShortEvent extends Equatable { } class RecipesShortLoaded extends RecipesShortEvent { - final String category; - const RecipesShortLoaded({required this.category}); + final String category; @override List get props => [category]; diff --git a/lib/src/blocs/recipes_short/recipes_short_state.dart b/lib/src/blocs/recipes_short/recipes_short_state.dart index b7b277b1..104f5be3 100644 --- a/lib/src/blocs/recipes_short/recipes_short_state.dart +++ b/lib/src/blocs/recipes_short/recipes_short_state.dart @@ -10,10 +10,6 @@ enum RecipesShortStatus { } class RecipesShortState extends Equatable { - final RecipesShortStatus status; - final String? error; - final Iterable? recipesShort; - RecipesShortState({ this.status = RecipesShortStatus.loadInProgress, this.error, @@ -34,6 +30,9 @@ class RecipesShortState extends Equatable { break; } } + final RecipesShortStatus status; + final String? error; + final Iterable? recipesShort; @override List get props => [status, error, recipesShort]; diff --git a/lib/src/models/animated_list.dart b/lib/src/models/animated_list.dart index 7c4e9a58..e479a4dd 100644 --- a/lib/src/models/animated_list.dart +++ b/lib/src/models/animated_list.dart @@ -27,13 +27,12 @@ abstract class AnimatedListModel { } E removeAt(int index) { - final E removedItem = _items.removeAt(index); + final removedItem = _items.removeAt(index); if (removedItem != null) { _animatedList.removeItem( index, - (BuildContext context, Animation animation) { - return removedItemBuilder(removedItem, context, animation); - }, + (context, animation) => + removedItemBuilder(removedItem, context, animation), ); } return removedItem; diff --git a/lib/src/models/app_authentication.dart b/lib/src/models/app_authentication.dart index 9f67350e..bae84a47 100644 --- a/lib/src/models/app_authentication.dart +++ b/lib/src/models/app_authentication.dart @@ -9,13 +9,6 @@ part 'app_authentication.g.dart'; @JsonSerializable() class AppAuthentication extends Equatable { - final String server; - final String loginName; - final String basicAuth; - final bool isSelfSignedCertificate; - - final Dio authenticatedClient = Dio(); - AppAuthentication({ required this.server, required this.loginName, @@ -23,8 +16,8 @@ class AppAuthentication extends Equatable { required this.isSelfSignedCertificate, }) { authenticatedClient.options - ..headers["authorization"] = basicAuth - ..headers["User-Agent"] = "Cookbook App" + ..headers['authorization'] = basicAuth + ..headers['User-Agent'] = 'Cookbook App' ..responseType = ResponseType.plain; if (isSelfSignedCertificate) { @@ -48,21 +41,27 @@ class AppAuthentication extends Equatable { // ignore: avoid_catching_errors } on TypeError { final basicAuth = parseBasicAuth( - jsonData["loginName"] as String, - jsonData["appPassword"] as String, + jsonData['loginName'] as String, + jsonData['appPassword'] as String, ); final selfSignedCertificate = jsonData['isSelfSignedCertificate'] as bool? ?? false; return AppAuthentication( - server: jsonData["server"] as String, - loginName: jsonData["loginName"] as String, + server: jsonData['server'] as String, + loginName: jsonData['loginName'] as String, basicAuth: basicAuth, isSelfSignedCertificate: selfSignedCertificate, ); } } + final String server; + final String loginName; + final String basicAuth; + final bool isSelfSignedCertificate; + + final Dio authenticatedClient = Dio(); String toJsonString() => json.encode(toJson()); Map toJson() => _$AppAuthenticationToJson(this); @@ -70,13 +69,12 @@ class AppAuthentication extends Equatable { String get password { final base64 = basicAuth.substring(6); final string = utf8.decode(base64Decode(base64)); - final auth = string.split(":"); + final auth = string.split(':'); return auth[1]; } - static String parseBasicAuth(String loginName, String appPassword) { - return 'Basic ${base64Encode(utf8.encode('$loginName:$appPassword'))}'; - } + static String parseBasicAuth(String loginName, String appPassword) => + 'Basic ${base64Encode(utf8.encode('$loginName:$appPassword'))}'; @override String toString() => diff --git a/lib/src/models/image_response.dart b/lib/src/models/image_response.dart index be65a836..cdf8ab54 100644 --- a/lib/src/models/image_response.dart +++ b/lib/src/models/image_response.dart @@ -1,11 +1,10 @@ import 'package:flutter/foundation.dart'; class ImageResponse { - final Uint8List data; - final bool isSvg; - const ImageResponse({ required this.data, required this.isSvg, }); + final Uint8List data; + final bool isSvg; } diff --git a/lib/src/models/recipe.dart b/lib/src/models/recipe.dart index 942afeea..733edb9e 100644 --- a/lib/src/models/recipe.dart +++ b/lib/src/models/recipe.dart @@ -2,42 +2,42 @@ import 'package:nc_cookbook_api/nc_cookbook_api.dart'; extension RecipeExtension on Recipe { Map get nutritionList { - final Map items = {}; + final items = {}; if (nutrition.calories != null) { - items["calories"] = nutrition.calories!; + items['calories'] = nutrition.calories!; } if (nutrition.carbohydrateContent != null) { - items["carbohydrateContent"] = nutrition.carbohydrateContent!; + items['carbohydrateContent'] = nutrition.carbohydrateContent!; } if (nutrition.cholesterolContent != null) { - items["cholesterolContent"] = nutrition.cholesterolContent!; + items['cholesterolContent'] = nutrition.cholesterolContent!; } if (nutrition.fatContent != null) { - items["fatContent"] = nutrition.fatContent!; + items['fatContent'] = nutrition.fatContent!; } if (nutrition.fiberContent != null) { - items["fiberContent"] = nutrition.fiberContent!; + items['fiberContent'] = nutrition.fiberContent!; } if (nutrition.proteinContent != null) { - items["proteinContent"] = nutrition.proteinContent!; + items['proteinContent'] = nutrition.proteinContent!; } if (nutrition.saturatedFatContent != null) { - items["saturatedFatContent"] = nutrition.saturatedFatContent!; + items['saturatedFatContent'] = nutrition.saturatedFatContent!; } if (nutrition.servingSize != null) { - items["servingSize"] = nutrition.servingSize!; + items['servingSize'] = nutrition.servingSize!; } if (nutrition.sodiumContent != null) { - items["sodiumContent"] = nutrition.sodiumContent!; + items['sodiumContent'] = nutrition.sodiumContent!; } if (nutrition.sugarContent != null) { - items["sugarContent"] = nutrition.sugarContent!; + items['sugarContent'] = nutrition.sugarContent!; } if (nutrition.transFatContent != null) { - items["transFatContent"] = nutrition.transFatContent!; + items['transFatContent'] = nutrition.transFatContent!; } if (nutrition.unsaturatedFatContent != null) { - items["unsaturatedFatContent"] = nutrition.unsaturatedFatContent!; + items['unsaturatedFatContent'] = nutrition.unsaturatedFatContent!; } return items; diff --git a/lib/src/models/timer.dart b/lib/src/models/timer.dart index be2a1856..5548347d 100644 --- a/lib/src/models/timer.dart +++ b/lib/src/models/timer.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes + import 'package:flutter/foundation.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -5,23 +7,8 @@ import 'package:nc_cookbook_api/nc_cookbook_api.dart'; part 'timer.g.dart'; -@JsonSerializable(constructor: "restore") +@JsonSerializable(constructor: 'restore') class Timer { - @visibleForTesting - @JsonKey( - toJson: _recipeToJson, - fromJson: _recipeFromJson, - ) - final Recipe? recipe; - final DateTime done; - - final String? _title; - final String? _body; - final Duration? _duration; - final String? _recipeId; - - int? id; - Timer(Recipe this.recipe) : _title = null, _body = null, @@ -67,6 +54,20 @@ class Timer { ); } } + @visibleForTesting + @JsonKey( + toJson: _recipeToJson, + fromJson: _recipeFromJson, + ) + final Recipe? recipe; + final DateTime done; + + final String? _title; + final String? _body; + final Duration? _duration; + final String? _recipeId; + + int? id; Map toJson() => _$TimerToJson(this); @@ -91,7 +92,7 @@ class Timer { /// Prgogrss of the timer in percent. double get progress { if (remaining == Duration.zero) { - return 1.0; + return 1; } return 1.0 - (remaining.inMicroseconds / duration.inMicroseconds); diff --git a/lib/src/screens/category_screen.dart b/lib/src/screens/category_screen.dart index fb90b541..d6675683 100644 --- a/lib/src/screens/category_screen.dart +++ b/lib/src/screens/category_screen.dart @@ -36,7 +36,7 @@ class _CategoryScreenState extends State { final theme = Theme.of(context).extension()!; try { - final APIVersion apiVersion = await UserRepository().fetchApiVersion(); + final apiVersion = await UserRepository().fetchApiVersion(); if (!UserRepository().isVersionSupported(apiVersion)) { // ignore: use_build_context_synchronously @@ -44,10 +44,10 @@ class _CategoryScreenState extends State { SnackBar( content: Text( translate( - "categories.errors.api_version_above_confirmed", + 'categories.errors.api_version_above_confirmed', args: { - "version": - "${apiVersion.epoch}.${apiVersion.major}.${apiVersion.minor}" + 'version': + '${apiVersion.epoch}.${apiVersion.major}.${apiVersion.minor}' }, ), ), @@ -60,8 +60,8 @@ class _CategoryScreenState extends State { SnackBar( content: Text( translate( - "categories.errors.api_version_check_failed", - args: {"error_msg": e}, + 'categories.errors.api_version_check_failed', + args: {'error_msg': e}, ), style: theme.errorSnackBar.contentTextStyle, ), @@ -77,89 +77,91 @@ class _CategoryScreenState extends State { BlocProvider.of(context).add(const CategoriesLoaded()); } - Widget bodyBuilder(BuildContext context, CategoriesState state) { - return RefreshIndicator( - onRefresh: refresh, - child: Builder( - builder: (context) { - switch (state.status) { - case CategoriesStatus.loadSuccess: - case CategoriesStatus.imageLoadSuccess: - final categories = state.categories!.toList(); - final recipe = state.recipes?.toList(); + Widget bodyBuilder(BuildContext context, CategoriesState state) => + RefreshIndicator( + onRefresh: refresh, + child: Builder( + builder: (context) { + switch (state.status) { + case CategoriesStatus.loadSuccess: + case CategoriesStatus.imageLoadSuccess: + final categories = state.categories!.toList(); + final recipe = state.recipes?.toList(); - final extent = CategoryCard.hightExtend(context); - return GridView.builder( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 75), - gridDelegate: CategoryGridDelegate(extent: extent), - semanticChildCount: categories.length, - itemCount: categories.length, - itemBuilder: (context, index) => CategoryCard( - categories[index], - recipe?[index]?.recipeId, - ), - ); - case CategoriesStatus.loadInProgress: - BlocProvider.of(context) - .add(const CategoriesLoaded()); + final extent = CategoryCard.hightExtend(context); + return GridView.builder( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 75), + gridDelegate: CategoryGridDelegate(extent: extent), + semanticChildCount: categories.length, + itemCount: categories.length, + itemBuilder: (context, index) => CategoryCard( + categories[index], + recipe?[index]?.recipeId, + ), + ); + case CategoriesStatus.loadInProgress: + BlocProvider.of(context) + .add(const CategoriesLoaded()); - return Center( - child: SpinKitWave( - color: Theme.of(context).colorScheme.primary, - ), - ); - case CategoriesStatus.loadFailure: - return Padding( - padding: const EdgeInsets.all(16), - child: 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': state.error}, + return Center( + child: SpinKitWave( + color: Theme.of(context).colorScheme.primary, + ), + ); + case CategoriesStatus.loadFailure: + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate('categories.errors.plugin_missing'), + style: const TextStyle(fontWeight: FontWeight.bold), ), - ), - ], - ), - ); - default: - throw StateError('invalid CategoryState'); - } - }, - ), - ); - } + const Divider(), + Text( + translate( + 'categories.errors.load_failed', + args: {'error_msg': state.error}, + ), + ), + ], + ), + ); + default: + throw StateError('invalid CategoryState'); + } + }, + ), + ); - Widget iconBuilder(BuildContext context, RecipesShortState state) { - return IconButton( - icon: Builder( - builder: (context) { - switch (state.status) { - case RecipesShortStatus.loadAllInProgress: - return const Icon(Icons.downloading_outlined); - case RecipesShortStatus.loadAllFailure: - return const Icon(Icons.report_problem_outlined); - default: - return const Icon(Icons.search_outlined); - } + Widget iconBuilder(BuildContext context, RecipesShortState state) => + IconButton( + icon: Builder( + builder: (context) { + switch (state.status) { + case RecipesShortStatus.loadAllInProgress: + return const Icon(Icons.downloading_outlined); + case RecipesShortStatus.loadAllFailure: + return const Icon(Icons.report_problem_outlined); + default: + return const Icon(Icons.search_outlined); + } + }, + ), + tooltip: MaterialLocalizations.of(context).searchFieldLabel, + onPressed: () async { + BlocProvider.of(context) + .add(RecipesShortLoadedAll()); }, - ), - tooltip: MaterialLocalizations.of(context).searchFieldLabel, - onPressed: () async { - BlocProvider.of(context).add(RecipesShortLoadedAll()); - }, - ); - } + ); - void recipeListener(BuildContext context, RecipesShortState state) { + Future recipeListener( + BuildContext context, + RecipesShortState state, + ) async { if (state.status == RecipesShortStatus.loadAllSuccess) { - showSearch( + await showSearch( context: context, delegate: SearchPage( items: state.recipesShort!.toList(), @@ -185,7 +187,7 @@ class _CategoryScreenState extends State { content: Text( translate( 'search.errors.search_failed', - args: {"error_msg": state.error}, + args: {'error_msg': state.error}, ), style: theme.contentTextStyle, ), @@ -196,42 +198,40 @@ class _CategoryScreenState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(translate('categories.title')), - actions: [ - BlocConsumer( - builder: iconBuilder, - listener: recipeListener, - ), - IconButton( - icon: const Icon(Icons.refresh_outlined), - tooltip: - MaterialLocalizations.of(context).refreshIndicatorSemanticLabel, - onPressed: refresh, - ), - ], - ), - drawer: const MainDrawer(), - body: BlocBuilder( - builder: bodyBuilder, - ), - floatingActionButton: FloatingActionButton( - tooltip: translate("recipe_edit.title").toLowerCase(), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlocProvider( - create: (context) => RecipeBloc(), - child: const RecipeEditScreen(), - ), + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(translate('categories.title')), + actions: [ + BlocConsumer( + builder: iconBuilder, + listener: recipeListener, ), - ); - }, - child: const Icon(Icons.add_outlined), - ), - ); - } + IconButton( + icon: const Icon(Icons.refresh_outlined), + tooltip: MaterialLocalizations.of(context) + .refreshIndicatorSemanticLabel, + onPressed: refresh, + ), + ], + ), + drawer: const MainDrawer(), + body: BlocBuilder( + builder: bodyBuilder, + ), + floatingActionButton: FloatingActionButton( + tooltip: translate('recipe_edit.title').toLowerCase(), + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BlocProvider( + create: (context) => RecipeBloc(), + child: const RecipeEditScreen(), + ), + ), + ); + }, + child: const Icon(Icons.add_outlined), + ), + ); } diff --git a/lib/src/screens/form/login_form.dart b/lib/src/screens/form/login_form.dart index 6a9cfda3..722c9569 100644 --- a/lib/src/screens/form/login_form.dart +++ b/lib/src/screens/form/login_form.dart @@ -16,6 +16,7 @@ class LoginForm extends StatefulWidget { State createState() => _LoginFormState(); } +// ignore: prefer_mixin class _LoginFormState extends State with WidgetsBindingObserver { final _serverUrl = TextEditingController(); final _username = TextEditingController(); @@ -64,10 +65,10 @@ class _LoginFormState extends State with WidgetsBindingObserver { _formKey.currentState?.save(); if (_formKey.currentState?.validate() ?? false) { - final String serverUrl = URLUtils.sanitizeUrl(_serverUrl.text); - final String username = _username.text.trim(); - final String password = _password.text.trim(); - final String originalBasicAuth = + final serverUrl = URLUtils.sanitizeUrl(_serverUrl.text); + final username = _username.text.trim(); + final password = _password.text.trim(); + final originalBasicAuth = AppAuthentication.parseBasicAuth(username, password); BlocProvider.of(context).add( @@ -99,146 +100,144 @@ class _LoginFormState extends State with WidgetsBindingObserver { } }, child: BlocBuilder( - builder: (context, state) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Form( - // Build a Form widget using the _formKey created above. - key: _formKey, - child: AutofillGroup( - child: Column( - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate('login.server_url.field'), - ), - controller: _serverUrl, - keyboardType: TextInputType.url, - validator: (value) { - if (value == null || value.isEmpty) { - return translate( - 'login.server_url.validator.empty', - ); - } + builder: (context, state) => SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: Form( + // Build a Form widget using the _formKey created above. + key: _formKey, + child: AutofillGroup( + child: Column( + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate('login.server_url.field'), + ), + controller: _serverUrl, + keyboardType: TextInputType.url, + validator: (value) { + if (value == null || value.isEmpty) { + return translate( + 'login.server_url.validator.empty', + ); + } - if (!URLUtils.isValid(value)) { - return translate( - 'login.server_url.validator.pattern', - ); - } - return null; - }, - textInputAction: TextInputAction.next, - autofillHints: const [ - AutofillHints.url, - AutofillHints.name - ], + if (!URLUtils.isValid(value)) { + return translate( + 'login.server_url.validator.pattern', + ); + } + return null; + }, + textInputAction: TextInputAction.next, + autofillHints: const [ + AutofillHints.url, + AutofillHints.name + ], + ), + TextFormField( + decoration: InputDecoration( + labelText: translate('login.username.field'), ), - TextFormField( - decoration: InputDecoration( - labelText: translate('login.username.field'), - ), - controller: _username, - textInputAction: TextInputAction.next, - autofillHints: const [AutofillHints.username], + controller: _username, + textInputAction: TextInputAction.next, + autofillHints: const [AutofillHints.username], + ), + TextFormField( + decoration: InputDecoration( + labelText: translate('login.password.field'), ), - TextFormField( - decoration: InputDecoration( - labelText: translate('login.password.field'), - ), - controller: _password, - obscureText: true, - onFieldSubmitted: (val) { - if (state.status != LoginStatus.loading) { - onLoginButtonPressed(); - } + controller: _password, + obscureText: true, + onFieldSubmitted: (val) { + if (state.status != LoginStatus.loading) { + onLoginButtonPressed(); + } + }, + textInputAction: TextInputAction.done, + autofillHints: const [AutofillHints.password], + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: ExpansionPanelList( + expandedHeaderPadding: EdgeInsets.zero, + expansionCallback: (index, isExpanded) { + setState(() { + advancedSettingsExpanded = !isExpanded; + }); }, - textInputAction: TextInputAction.done, - autofillHints: const [AutofillHints.password], - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: ExpansionPanelList( - expandedHeaderPadding: EdgeInsets.zero, - expansionCallback: (int index, bool isExpanded) { - setState(() { - advancedSettingsExpanded = !isExpanded; - }); - }, - children: [ - ExpansionPanel( - isExpanded: advancedSettingsExpanded, - body: Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Column( - children: [ - CheckboxFormField( - initialValue: advancedIsAppPassword, - onSaved: (bool? checked) { - if (checked == null) return; - setState(() { - advancedIsAppPassword = checked; - }); - }, - title: Text( - translate( - 'login.settings.app_password', - ), + children: [ + ExpansionPanel( + isExpanded: advancedSettingsExpanded, + body: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + children: [ + CheckboxFormField( + initialValue: advancedIsAppPassword, + onSaved: (checked) { + if (checked == null) { + return; + } + setState(() { + advancedIsAppPassword = checked; + }); + }, + title: Text( + translate( + 'login.settings.app_password', ), ), - CheckboxFormField( - initialValue: - advancedIsSelfSignedCertificate, - onSaved: (bool? checked) { - if (checked == null) return; + ), + CheckboxFormField( + initialValue: + advancedIsSelfSignedCertificate, + onSaved: (checked) { + if (checked == null) { + return; + } - setState(() { - advancedIsSelfSignedCertificate = - checked; - }); - }, - title: Text( - translate( - 'login.settings.self_signed_certificate', - ), + setState(() { + advancedIsSelfSignedCertificate = + checked; + }); + }, + title: Text( + translate( + 'login.settings.self_signed_certificate', ), ), - ], - ), - ), - headerBuilder: - (BuildContext context, bool isExpanded) { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: - Text(translate('login.settings.title')), ), - ); - }, - ) - ], - ), + ], + ), + ), + headerBuilder: (context, isExpanded) => Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: Text(translate('login.settings.title')), + ), + ), + ) + ], ), - ElevatedButton( - onPressed: state.status != LoginStatus.loading - ? onLoginButtonPressed - : null, - child: Text(translate('login.button')), + ), + ElevatedButton( + onPressed: state.status != LoginStatus.loading + ? onLoginButtonPressed + : null, + child: Text(translate('login.button')), + ), + if (state.status == LoginStatus.loading) + SpinKitWave( + color: Theme.of(context).colorScheme.primary, ), - if (state.status == LoginStatus.loading) - SpinKitWave( - color: Theme.of(context).colorScheme.primary, - ), - ], - ), + ], ), ), ), - ); - }, + ), + ), ), ); } diff --git a/lib/src/screens/loading_screen.dart b/lib/src/screens/loading_screen.dart index cd49a401..b6e24558 100644 --- a/lib/src/screens/loading_screen.dart +++ b/lib/src/screens/loading_screen.dart @@ -9,35 +9,33 @@ class LoadingErrorScreen extends StatelessWidget { final String message; @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(message), - const SizedBox(height: 10), - ElevatedButton( - onPressed: () { - BlocProvider.of(context) - .add(const AppStarted()); - }, - child: Text(translate("login.retry")), - ), - const SizedBox(height: 10), - ElevatedButton( - onPressed: () { - BlocProvider.of(context) - .add(const LoggedOut()); - }, - child: Text(translate("login.reset")), - ), - ], + Widget build(BuildContext context) => Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(message), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () { + BlocProvider.of(context) + .add(const AppStarted()); + }, + child: Text(translate('login.retry')), + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () { + BlocProvider.of(context) + .add(const LoggedOut()); + }, + child: Text(translate('login.reset')), + ), + ], + ), ), ), - ), - ); - } + ); } diff --git a/lib/src/screens/login_screen.dart b/lib/src/screens/login_screen.dart index f1447272..8d6f10b6 100644 --- a/lib/src/screens/login_screen.dart +++ b/lib/src/screens/login_screen.dart @@ -8,31 +8,29 @@ import 'package:nextcloud_cookbook_flutter/src/screens/form/login_form.dart'; import 'package:nextcloud_cookbook_flutter/src/util/theme_data.dart'; class LoginScreen extends StatelessWidget { - final bool invalidCredentials; const LoginScreen({ super.key, this.invalidCredentials = false, }); + final bool invalidCredentials; @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(translate('login.title')), - ), - body: BlocProvider( - create: (context) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - notifyIfInvalidCredentials(context); - }); - return LoginBloc( - authenticationBloc: BlocProvider.of(context), - ); - }, - child: const LoginForm(), - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(translate('login.title')), + ), + body: BlocProvider( + create: (context) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + notifyIfInvalidCredentials(context); + }); + return LoginBloc( + authenticationBloc: BlocProvider.of(context), + ); + }, + child: const LoginForm(), + ), + ); void notifyIfInvalidCredentials(BuildContext context) { if (invalidCredentials) { diff --git a/lib/src/screens/my_settings_screen.dart b/lib/src/screens/my_settings_screen.dart index 2bba58f5..f35ef6ba 100644 --- a/lib/src/screens/my_settings_screen.dart +++ b/lib/src/screens/my_settings_screen.dart @@ -11,53 +11,52 @@ class MySettingsScreen extends StatelessWidget { const MySettingsScreen({super.key}); @override - Widget build(BuildContext context) { - return SettingsScreen( - title: translate("settings.title"), - children: [ - SwitchSettingsTile( - title: translate("settings.stay_awake.title"), - settingKey: SettingKeys.stay_awake.name, - subtitle: translate("settings.stay_awake.subtitle"), - ), - DropDownSettingsTile( - title: translate("settings.dark_mode.title"), - settingKey: SettingKeys.dark_mode.name, - values: { - ThemeMode.system.toString(): translate("settings.dark_mode.system"), - ThemeMode.dark.toString(): translate("settings.dark_mode.dark"), - ThemeMode.light.toString(): translate("settings.dark_mode.light"), - }, - selected: ThemeModeHandler.of(context)!.themeMode.toString(), - onChange: (value) { - final theme = ThemeMode.values.firstWhere( - (v) => v.toString() == value, - orElse: () => ThemeMode.system, - ); - ThemeModeHandler.of(context)?.saveThemeMode(theme); - }, - ), - DropDownSettingsTile( - title: translate("settings.language.title"), - settingKey: SettingKeys.language.name, - selected: Settings.getValue( - SettingKeys.language.name, - defaultValue: 'default', + Widget build(BuildContext context) => SettingsScreen( + title: translate('settings.title'), + children: [ + SwitchSettingsTile( + title: translate('settings.stay_awake.title'), + settingKey: SettingKeys.stay_awake.name, + subtitle: translate('settings.stay_awake.subtitle'), ), - values: Map.from( - { - 'default': translate("settings.dark_mode.system"), + DropDownSettingsTile( + title: translate('settings.dark_mode.title'), + settingKey: SettingKeys.dark_mode.name, + values: { + ThemeMode.system.toString(): + translate('settings.dark_mode.system'), + ThemeMode.dark.toString(): translate('settings.dark_mode.dark'), + ThemeMode.light.toString(): translate('settings.dark_mode.light'), }, - )..addAll(SupportedLocales.locales), - onChange: (dynamic value) { - if (value == 'default') { - changeLocale(context, Platform.localeName); - } else { - changeLocale(context, value as String?); - } - }, - ) - ], - ); - } + selected: ThemeModeHandler.of(context)!.themeMode.toString(), + onChange: (value) async { + final theme = ThemeMode.values.firstWhere( + (v) => v.toString() == value, + orElse: () => ThemeMode.system, + ); + await ThemeModeHandler.of(context)?.saveThemeMode(theme); + }, + ), + DropDownSettingsTile( + title: translate('settings.language.title'), + settingKey: SettingKeys.language.name, + selected: Settings.getValue( + SettingKeys.language.name, + defaultValue: 'default', + )!, + values: Map.from( + { + 'default': translate('settings.dark_mode.system'), + }, + )..addAll(SupportedLocales.locales), + onChange: (value) async { + if (value == 'default') { + await changeLocale(context, Platform.localeName); + } else { + await changeLocale(context, value); + } + }, + ) + ], + ); } diff --git a/lib/src/screens/recipe_edit_screen.dart b/lib/src/screens/recipe_edit_screen.dart index 1e85016a..27b9b717 100644 --- a/lib/src/screens/recipe_edit_screen.dart +++ b/lib/src/screens/recipe_edit_screen.dart @@ -16,12 +16,11 @@ import 'package:nextcloud_cookbook_flutter/src/widget/input/integer_text_form_fi import 'package:nextcloud_cookbook_flutter/src/widget/input/reorderable_list_form_field.dart'; class RecipeEditScreen extends StatefulWidget { - final Recipe? recipe; - const RecipeEditScreen({ this.recipe, super.key, }); + final Recipe? recipe; @override State createState() => _RecipeEditScreenState(); @@ -85,7 +84,7 @@ class _RecipeEditScreenState extends State { Future onWillPop() async { if (widget.recipe != null) { - final RecipeBloc recipeBloc = BlocProvider.of(context); + final recipeBloc = BlocProvider.of(context); if (recipeBloc.state.status == RecipeStatus.updateFailure) { recipeBloc.add(RecipeLoaded(widget.recipe!.id!)); } @@ -104,32 +103,30 @@ class _RecipeEditScreenState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(translate('$translationKey.title')), - actions: [ - if (widget.recipe != null) ...[ - IconButton( - icon: const Icon(Icons.delete), - tooltip: translate("recipe_edit.delete.title").toLowerCase(), - color: Theme.of(context).colorScheme.error, - onPressed: onDelete, - ), + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(translate('$translationKey.title')), + actions: [ + if (widget.recipe != null) ...[ + IconButton( + icon: const Icon(Icons.delete), + tooltip: translate('recipe_edit.delete.title').toLowerCase(), + color: Theme.of(context).colorScheme.error, + onPressed: onDelete, + ), + ], ], - ], - ), - body: BlocConsumer( - builder: builder, - listener: listener, - ), - floatingActionButton: FloatingActionButton( - tooltip: translate('$translationKey.button'), - onPressed: onSubmit, - child: const Icon(Icons.check_outlined), - ), - ); - } + ), + body: BlocConsumer( + builder: builder, + listener: listener, + ), + floatingActionButton: FloatingActionButton( + tooltip: translate('$translationKey.button'), + onPressed: onSubmit, + child: const Icon(Icons.check_outlined), + ), + ); Widget builder(BuildContext context, RecipeState state) { final theme = Theme.of(context); @@ -155,7 +152,7 @@ class _RecipeEditScreenState extends State { autofocus: true, validator: (value) { if (value == null || value.isEmpty) { - return translate("form.validators.required"); + return translate('form.validators.required'); } return null; @@ -185,13 +182,10 @@ class _RecipeEditScreenState extends State { textInputAction: TextInputAction.next, ), suggestionsCallback: DataRepository().getMatchingCategoryNames, - itemBuilder: (context, String suggestion) { - return ListTile( - title: Text(suggestion), - ); - }, - onSuggestionSelected: (String? suggestion) { - if (suggestion == null) return; + itemBuilder: (context, suggestion) => ListTile( + title: Text(suggestion), + ), + onSuggestionSelected: (suggestion) { categoryController.text = suggestion; }, onSaved: (value) => _mutableRecipe.recipeCategory = value, @@ -277,7 +271,7 @@ class _RecipeEditScreenState extends State { if (state.status == RecipeStatus.updateInProgress) SpinKitWave( color: Theme.of(context).colorScheme.onSecondary, - size: 30.0, + size: 30, ), ]; @@ -328,7 +322,7 @@ class _RecipeEditScreenState extends State { ); } - void listener(BuildContext context, RecipeState state) { + Future listener(BuildContext context, RecipeState state) async { final theme = Theme.of(context).extension()!.errorSnackBar; switch (state.status) { @@ -340,7 +334,7 @@ class _RecipeEditScreenState extends State { content: Text( translate( '$translationKey.errors.update_failed', - args: {"error_msg": state.error}, + args: {'error_msg': state.error}, ), style: theme.contentTextStyle, ), @@ -358,7 +352,7 @@ class _RecipeEditScreenState extends State { Navigator.pop(context); break; case RecipeStatus.createSuccess: - Navigator.pushReplacement( + await Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => RecipeScreen(recipeId: state.recipeId!), diff --git a/lib/src/screens/recipe_import_screen.dart b/lib/src/screens/recipe_import_screen.dart index 98709a86..0befc28a 100644 --- a/lib/src/screens/recipe_import_screen.dart +++ b/lib/src/screens/recipe_import_screen.dart @@ -10,9 +10,12 @@ import 'package:nextcloud_cookbook_flutter/src/util/theme_data.dart'; import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart'; class RecipeImportScreen extends StatefulWidget { - final String importUrl; + const RecipeImportScreen({ + this.importUrl = '', + super.key, + }); - const RecipeImportScreen([this.importUrl = '']); + final String importUrl; @override State createState() => _RecipeImportScreenState(); @@ -78,22 +81,20 @@ class _RecipeImportScreenState extends State { } @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => RecipeBloc(), - child: Scaffold( - appBar: AppBar( - title: Text(translate("recipe_import.title")), - ), - body: BlocConsumer( - builder: builder, - listener: listener, + Widget build(BuildContext context) => BlocProvider( + create: (context) => RecipeBloc(), + child: Scaffold( + appBar: AppBar( + title: Text(translate('recipe_import.title')), + ), + body: BlocConsumer( + builder: builder, + listener: listener, + ), ), - ), - ); - } + ); - void listener(BuildContext context, RecipeState state) { + Future listener(BuildContext context, RecipeState state) async { if (state.status == RecipeStatus.importFailure) { final theme = Theme.of(context).extension()!.errorSnackBar; @@ -102,7 +103,7 @@ class _RecipeImportScreenState extends State { content: Text( translate( 'recipe_import.errors.import_failed', - args: {"error_msg": state.error}, + args: {'error_msg': state.error}, ), style: theme.contentTextStyle, ), @@ -110,12 +111,10 @@ class _RecipeImportScreenState extends State { ), ); } else if (state.status == RecipeStatus.importSuccess) { - Navigator.push( + await Navigator.push( context, MaterialPageRoute( - builder: (context) { - return RecipeScreen(recipeId: state.recipe!.id!); - }, + builder: (context) => RecipeScreen(recipeId: state.recipe!.id!), ), ); } @@ -126,7 +125,7 @@ class _RecipeImportScreenState extends State { return SingleChildScrollView( child: Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.all(10), child: Form( key: _formKey, child: Column( @@ -138,7 +137,7 @@ class _RecipeImportScreenState extends State { onSaved: import, onEditingComplete: onSubmit, decoration: InputDecoration( - hintText: translate("recipe_import.field"), + hintText: translate('recipe_import.field'), suffixIcon: IconButton( tooltip: MaterialLocalizations.of(context).pasteButtonLabel, onPressed: pasteClipboard, @@ -152,7 +151,7 @@ class _RecipeImportScreenState extends State { ? OutlinedButton.icon( onPressed: enabled ? onSubmit : null, icon: const Icon(Icons.cloud_download_outlined), - label: Text(translate("recipe_import.button")), + label: Text(translate('recipe_import.button')), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 25), side: BorderSide( @@ -162,7 +161,7 @@ class _RecipeImportScreenState extends State { ) : SpinKitWave( color: Theme.of(context).colorScheme.primary, - size: 30.0, + size: 30, ), ), ], diff --git a/lib/src/screens/recipe_screen.dart b/lib/src/screens/recipe_screen.dart index 5e797277..755832ac 100644 --- a/lib/src/screens/recipe_screen.dart +++ b/lib/src/screens/recipe_screen.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; @@ -14,22 +16,19 @@ import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/timer_list_item.dart'; class RecipeScreen extends StatelessWidget { - final String recipeId; - const RecipeScreen({ required this.recipeId, super.key, }); + final String recipeId; @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => RecipeBloc()..add(RecipeLoaded(recipeId)), - child: BlocBuilder( - builder: builder, - ), - ); - } + Widget build(BuildContext context) => BlocProvider( + create: (context) => RecipeBloc()..add(RecipeLoaded(recipeId)), + child: BlocBuilder( + builder: builder, + ), + ); Widget builder(BuildContext context, RecipeState state) { switch (state.status) { @@ -75,7 +74,7 @@ class _RecipeScreenBodyState extends State { late AnimatedTimerList _list; Future _onEdit() async { - disableWakelock(); + unawaited(disableWakelock()); final recipeBloc = BlocProvider.of(context); await Navigator.push( @@ -87,43 +86,41 @@ class _RecipeScreenBodyState extends State { ), ), ); - enableWakelock(); + unawaited(enableWakelock()); } Widget _buildTimerItem( BuildContext context, int index, Animation animation, - ) { - return TimerListItem( - animation: animation, - item: _list[index], - dense: true, - onDismissed: () { - _list.removeAt(index); - }, - ); - } + ) => + TimerListItem( + animation: animation, + item: _list[index], + dense: true, + onDismissed: () { + _list.removeAt(index); + }, + ); Widget _buildRemovedTimerItem( Timer item, BuildContext context, Animation animation, - ) { - return TimerListItem( - animation: animation, - item: item, - dense: true, - enabled: false, - ); - } + ) => + TimerListItem( + animation: animation, + item: item, + dense: true, + enabled: false, + ); @override void initState() { super.initState(); recipe = widget.recipe; - enableWakelock(); + unawaited(enableWakelock()); _list = AnimatedTimerList.forId( listKey: _listKey, @@ -174,7 +171,10 @@ class _RecipeScreenBodyState extends State { ); final body = WillPopScope( - onWillPop: disableWakelock, + onWillPop: () async { + unawaited(disableWakelock()); + return true; + }, child: Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: CustomScrollView( @@ -186,15 +186,15 @@ class _RecipeScreenBodyState extends State { ), ), SliverPadding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(16), sliver: header, ), SliverPadding( - padding: const EdgeInsets.only(bottom: 16.0), + padding: const EdgeInsets.only(bottom: 16), sliver: timerList, ), SliverPadding( - padding: const EdgeInsets.only(bottom: 75.0), + padding: const EdgeInsets.only(bottom: 75), sliver: bottom, ), ], @@ -207,7 +207,7 @@ class _RecipeScreenBodyState extends State { actions: [ IconButton( icon: const Icon(Icons.edit_outlined), - tooltip: translate("recipe_create.title").toLowerCase(), + tooltip: translate('recipe_create.title').toLowerCase(), onPressed: _onEdit, ), ], @@ -253,7 +253,7 @@ class RecipeScreenFab extends StatelessWidget { return FloatingActionButton( onPressed: callback, - tooltip: translate("timer.button.start"), + tooltip: translate('timer.button.start'), backgroundColor: color, child: const Icon(Icons.access_alarm_outlined), ); diff --git a/lib/src/screens/recipes_list_screen.dart b/lib/src/screens/recipes_list_screen.dart index b5d2234b..4fa05a3e 100644 --- a/lib/src/screens/recipes_list_screen.dart +++ b/lib/src/screens/recipes_list_screen.dart @@ -6,12 +6,11 @@ import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short import 'package:nextcloud_cookbook_flutter/src/widget/recipe_list_item.dart'; class RecipesListScreen extends StatefulWidget { - final String category; - const RecipesListScreen({ - super.key, required this.category, + super.key, }); + final String category; @override State createState() => _RecipesListScreenState(); @@ -19,7 +18,8 @@ class RecipesListScreen extends StatefulWidget { class _RecipesListScreenState extends State { Future refresh() async { - DefaultCacheManager().emptyCache(); + await DefaultCacheManager().emptyCache(); + // ignore: use_build_context_synchronously BlocProvider.of(context) .add(RecipesShortLoaded(category: widget.category)); } @@ -33,46 +33,44 @@ class _RecipesListScreenState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text( - translate( - 'recipe_list.title_category', - args: {'category': widget.category}, + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text( + translate( + 'recipe_list.title_category', + args: {'category': widget.category}, + ), ), + actions: [ + // action button + IconButton( + icon: const Icon(Icons.refresh_outlined), + tooltip: MaterialLocalizations.of(context) + .refreshIndicatorSemanticLabel, + onPressed: refresh, + ), + ], ), - actions: [ - // action button - IconButton( - icon: const Icon(Icons.refresh_outlined), - tooltip: - MaterialLocalizations.of(context).refreshIndicatorSemanticLabel, - onPressed: refresh, - ), - ], - ), - body: RefreshIndicator( - onRefresh: refresh, - child: BlocBuilder( - builder: (context, recipesShortState) { - if (recipesShortState.status == RecipesShortStatus.loadSuccess) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: ListView.separated( - itemCount: recipesShortState.recipesShort!.length, - itemBuilder: (context, index) => RecipeListItem( - recipe: recipesShortState.recipesShort!.elementAt(index), + body: RefreshIndicator( + onRefresh: refresh, + child: BlocBuilder( + builder: (context, recipesShortState) { + if (recipesShortState.status == RecipesShortStatus.loadSuccess) { + return Padding( + padding: const EdgeInsets.all(8), + child: ListView.separated( + itemCount: recipesShortState.recipesShort!.length, + itemBuilder: (context, index) => RecipeListItem( + recipe: recipesShortState.recipesShort!.elementAt(index), + ), + separatorBuilder: (context, index) => const Divider(), ), - separatorBuilder: (context, index) => const Divider(), - ), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), ), - ), - ); - } + ); } diff --git a/lib/src/screens/splash_screen.dart b/lib/src/screens/splash_screen.dart index 0308859b..c55b05dd 100644 --- a/lib/src/screens/splash_screen.dart +++ b/lib/src/screens/splash_screen.dart @@ -5,13 +5,11 @@ class SplashPage extends StatelessWidget { const SplashPage({super.key}); @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: SpinKitWave( - color: Theme.of(context).colorScheme.primary, + Widget build(BuildContext context) => Scaffold( + body: Center( + child: SpinKitWave( + color: Theme.of(context).colorScheme.primary, + ), ), - ), - ); - } + ); } diff --git a/lib/src/screens/timer_screen.dart b/lib/src/screens/timer_screen.dart index c5b7ec0a..e006526a 100644 --- a/lib/src/screens/timer_screen.dart +++ b/lib/src/screens/timer_screen.dart @@ -29,58 +29,54 @@ class _TimerScreenState extends State { BuildContext context, int index, Animation animation, - ) { - return TimerListItem( - animation: animation, - item: _list[index], - onDismissed: () { - _list.removeAt(index); - }, - ); - } + ) => + TimerListItem( + animation: animation, + item: _list[index], + onDismissed: () { + _list.removeAt(index); + }, + ); Widget _buildRemovedItem( Timer item, BuildContext context, Animation animation, - ) { - return TimerListItem( - animation: animation, - item: item, - enabled: false, - ); - } + ) => + TimerListItem( + animation: animation, + item: item, + enabled: false, + ); @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(translate('timer.title')), - actions: [ - if (_list.isNotEmpty) - IconButton( - icon: const Icon(Icons.clear_all_outlined), - tooltip: translate('app_bar.clear_all'), - onPressed: _list.removeAll, - ), - ], - ), - body: _list.isNotEmpty - ? CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.only(bottom: 16.0), - sliver: SliverAnimatedList( - key: _listKey, - initialItemCount: _list.length, - itemBuilder: _buildItem, + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(translate('timer.title')), + actions: [ + if (_list.isNotEmpty) + IconButton( + icon: const Icon(Icons.clear_all_outlined), + tooltip: translate('app_bar.clear_all'), + onPressed: _list.removeAll, + ), + ], + ), + body: _list.isNotEmpty + ? CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.only(bottom: 16), + sliver: SliverAnimatedList( + key: _listKey, + initialItemCount: _list.length, + itemBuilder: _buildItem, + ), ), - ), - ], - ) - : Center( - child: Text(translate('timer.empty_list')), - ), - ); - } + ], + ) + : Center( + child: Text(translate('timer.empty_list')), + ), + ); } diff --git a/lib/src/services/api_provider.dart b/lib/src/services/api_provider.dart index c0f07007..6c8705ed 100644 --- a/lib/src/services/api_provider.dart +++ b/lib/src/services/api_provider.dart @@ -1,8 +1,6 @@ part of 'services.dart'; class ApiProvider { - static final ApiProvider _apiProvider = ApiProvider._(); - factory ApiProvider() => _apiProvider; ApiProvider._() { final auth = UserRepository().currentAppAuthentication; @@ -13,7 +11,10 @@ class ApiProvider { connectTimeout: const Duration(milliseconds: 30000), receiveTimeout: const Duration(milliseconds: 30000), ), - ); + )..httpClientAdapter = IOHttpClientAdapter( + onHttpClientCreate: (client) => + client..badCertificateCallback = (cert, host, port) => true, + ); if (auth.isSelfSignedCertificate) { client.httpClientAdapter = IOHttpClientAdapter( @@ -27,7 +28,7 @@ class ApiProvider { ); ncCookbookApi.setBasicAuth( - "app_password", + 'app_password', auth.loginName, auth.password, ); @@ -36,6 +37,7 @@ class ApiProvider { miscApi = ncCookbookApi.getMiscApi(); tagsApi = ncCookbookApi.getTagsApi(); } + static final ApiProvider _apiProvider = ApiProvider._(); late NcCookbookApi ncCookbookApi; late RecipesApi recipeApi; diff --git a/lib/src/services/authentication_provider.dart b/lib/src/services/authentication_provider.dart index 6651d661..4f7c47cf 100644 --- a/lib/src/services/authentication_provider.dart +++ b/lib/src/services/authentication_provider.dart @@ -14,11 +14,11 @@ class AuthenticationProvider { }) async { assert(URLUtils.isSanitized(serverUrl)); - final String urlInitialCall = '$serverUrl/ocs/v2.php/core/getapppassword'; + final urlInitialCall = '$serverUrl/ocs/v2.php/core/getapppassword'; dio.Response response; try { - final dio.Dio client = dio.Dio(); + final client = dio.Dio(); if (isSelfSignedCertificate) { client.httpClientAdapter = IOHttpClientAdapter( onHttpClientCreate: (client) { @@ -32,27 +32,27 @@ class AuthenticationProvider { urlInitialCall, options: dio.Options( headers: { - "OCS-APIREQUEST": "true", - "User-Agent": "Cookbook App", - "authorization": originalBasicAuth + 'OCS-APIREQUEST': 'true', + 'User-Agent': 'Cookbook App', + 'authorization': originalBasicAuth }, validateStatus: (status) => status! < 500, ), cancelToken: _cancelToken, ); } on dio.DioError catch (e) { - if (e.message?.contains("SocketException") ?? false) { + if (e.message?.contains('SocketException') ?? false) { throw translate( - "login.errors.not_reachable", - args: {"server_url": serverUrl, "error_msg": e}, + 'login.errors.not_reachable', + args: {'server_url': serverUrl, 'error_msg': e}, ); - } else if (e.message?.contains("CERTIFICATE_VERIFY_FAILED") ?? false) { + } else if (e.message?.contains('CERTIFICATE_VERIFY_FAILED') ?? false) { throw translate( - "login.errors.certificate_failed", - args: {"server_url": serverUrl, "error_msg": e}, + 'login.errors.certificate_failed', + args: {'server_url': serverUrl, 'error_msg': e}, ); } - throw translate("login.errors.request_failed", args: {"error_msg": e}); + throw translate('login.errors.request_failed', args: {'error_msg': e}); } _cancelToken = null; @@ -60,14 +60,14 @@ class AuthenticationProvider { String appPassword; try { appPassword = XmlDocument.parse(response.data as String) - .findAllElements("apppassword") + .findAllElements('apppassword') .first .text; } on XmlParserException catch (e) { - throw translate("login.errors.parse_failed", args: {"error_msg": e}); + throw translate('login.errors.parse_failed', args: {'error_msg': e}); // ignore: avoid_catching_errors } on StateError catch (e) { - throw translate("login.errors.parse_missing", args: {"error_msg": e}); + throw translate('login.errors.parse_missing', args: {'error_msg': e}); } final basicAuth = AppAuthentication.parseBasicAuth(username, appPassword); @@ -79,13 +79,13 @@ class AuthenticationProvider { isSelfSignedCertificate: isSelfSignedCertificate, ); } else if (response.statusCode == 401) { - throw translate("login.errors.auth_failed"); + throw translate('login.errors.auth_failed'); } else { throw translate( - "login.errors.failure", + 'login.errors.failure', args: { - "status_code": response.statusCode, - "status_message": response.statusMessage, + 'status_code': response.statusCode, + 'status_message': response.statusMessage, }, ); } @@ -107,18 +107,18 @@ class AuthenticationProvider { isSelfSignedCertificate: isSelfSignedCertificate, ); } on dio.DioError catch (e) { - if (e.message?.contains("SocketException") ?? false) { + if (e.message?.contains('SocketException') ?? false) { throw translate( - "login.errors.not_reachable", - args: {"server_url": serverUrl, "error_msg": e}, + 'login.errors.not_reachable', + args: {'server_url': serverUrl, 'error_msg': e}, ); - } else if (e.message?.contains("CERTIFICATE_VERIFY_FAILED") ?? false) { + } else if (e.message?.contains('CERTIFICATE_VERIFY_FAILED') ?? false) { throw translate( - "login.errors.certificate_failed", - args: {"server_url": serverUrl, "error_msg": e}, + 'login.errors.certificate_failed', + args: {'server_url': serverUrl, 'error_msg': e}, ); } - throw translate("login.errors.request_failed", args: {"error_msg": e}); + throw translate('login.errors.request_failed', args: {'error_msg': e}); } if (authenticated) { @@ -129,12 +129,12 @@ class AuthenticationProvider { isSelfSignedCertificate: isSelfSignedCertificate, ); } else { - throw translate("login.errors.auth_failed"); + throw translate('login.errors.auth_failed'); } } void stopAuthenticate() { - _cancelToken?.cancel("Stopped by the User!"); + _cancelToken?.cancel('Stopped by the User!'); _cancelToken = null; } @@ -142,14 +142,14 @@ class AuthenticationProvider { if (currentAppAuthentication != null) { return true; } else { - final String? appAuthentication = + final appAuthentication = await _secureStorage.read(key: _appAuthenticationKey); return appAuthentication != null; } } Future loadAppAuthentication() async { - final String? appAuthenticationString = + final appAuthenticationString = await _secureStorage.read(key: _appAuthenticationKey); if (appAuthenticationString == null) { throw translate('login.errors.authentication_not_found'); @@ -165,12 +165,11 @@ class AuthenticationProvider { String basicAuth, { required bool isSelfSignedCertificate, }) async { - final String urlAuthCheck = - '$serverUrl/index.php/apps/cookbook/api/v1/categories'; + final urlAuthCheck = '$serverUrl/index.php/apps/cookbook/api/v1/categories'; dio.Response response; try { - final dio.Dio client = dio.Dio(); + final client = dio.Dio(); if (isSelfSignedCertificate) { client.httpClientAdapter = IOHttpClientAdapter( onHttpClientCreate: (client) { @@ -182,15 +181,15 @@ class AuthenticationProvider { response = await client.get( urlAuthCheck, options: dio.Options( - headers: {"authorization": basicAuth}, + headers: {'authorization': basicAuth}, validateStatus: (status) => status! < 500, ), ); } on dio.DioError catch (e) { throw translate( - "login.errors.no_internet", + 'login.errors.no_internet', args: { - "error_msg": e.message, + 'error_msg': e.message, }, ); } @@ -201,9 +200,9 @@ class AuthenticationProvider { return true; } else { throw translate( - "login.errors.wrong_status", + 'login.errors.wrong_status', args: { - "error_msg": response.statusCode, + 'error_msg': response.statusCode, }, ); } @@ -223,10 +222,10 @@ class AuthenticationProvider { dio.Response? response; try { response = await currentAppAuthentication?.authenticatedClient.delete( - "${currentAppAuthentication!.server}/ocs/v2.php/core/apppassword", + '${currentAppAuthentication!.server}/ocs/v2.php/core/apppassword', options: dio.Options( headers: { - "OCS-APIREQUEST": "true", + 'OCS-APIREQUEST': 'true', }, ), ); diff --git a/lib/src/services/data_repository.dart b/lib/src/services/data_repository.dart index 7ad4b927..9e689b1f 100644 --- a/lib/src/services/data_repository.dart +++ b/lib/src/services/data_repository.dart @@ -1,11 +1,11 @@ part of 'services.dart'; class DataRepository { - // Singleton - static final DataRepository _dataRepository = DataRepository._(); factory DataRepository() => _dataRepository; DataRepository._(); + // Singleton + static final DataRepository _dataRepository = DataRepository._(); // Provider List final ApiProvider api = ApiProvider(); @@ -25,7 +25,7 @@ class DataRepository { final response = await api.recipeApi.listRecipes(); return response.data; } else if (category == categoryUncategorized) { - final response = await api.categoryApi.recipesInCategory(category: "_"); + final response = await api.categoryApi.recipesInCategory(category: '_'); return response.data; } else { final response = @@ -83,9 +83,10 @@ class DataRepository { final uncategorizedBuilder = CategoryBuilder(); categories?.removeWhere((c) { - if (c.name == "*") { - uncategorizedBuilder.replace(c); - uncategorizedBuilder.name = categoryUncategorized; + if (c.name == '*') { + uncategorizedBuilder + ..replace(c) + ..name = categoryUncategorized; return true; } return false; @@ -99,7 +100,9 @@ class DataRepository { Future?> fetchCategoryMainRecipes( Iterable? categories, ) async { - if (categories == null) return null; + if (categories == null) { + return null; + } return Future.wait(categories.map(_fetchCategoryMainRecipe)); } @@ -111,20 +114,17 @@ class DataRepository { return categoryRecipes.first; } } catch (e) { - log("Could not load main recipe of Category!"); + log('Could not load main recipe of Category!'); rethrow; } return null; } - Future?> fetchAllRecipes() async { - return fetchRecipesShort(category: categoryAll); - } + Future?> fetchAllRecipes() async => + fetchRecipesShort(category: categoryAll); - String getUserAvatarUrl() { - return _nextcloudMetadataApi.getUserAvatarUrl(); - } + String getUserAvatarUrl() => _nextcloudMetadataApi.getUserAvatarUrl(); Future> getMatchingCategoryNames(String pattern) async { final categories = await fetchCategories(); @@ -137,24 +137,24 @@ class DataRepository { Future fetchImage(String recipeId, Size size) async { final String sizeParam; if (size.longestSide <= 16) { - sizeParam = "thumb16"; + sizeParam = 'thumb16'; } else if (size.longestSide <= 250) { - sizeParam = "thumb"; + sizeParam = 'thumb'; } else { - sizeParam = "full"; + sizeParam = 'full'; } final response = await api.recipeApi.getImage( id: recipeId, headers: { - "Accept": "image/jpeg, image/svg+xml", + 'Accept': 'image/jpeg, image/svg+xml', }, size: sizeParam, ); if (response.data != null) { return ImageResponse( data: response.data!, - isSvg: response.headers.value("content-type") == "image/svg+xml", + isSvg: response.headers.value('content-type') == 'image/svg+xml', ); } diff --git a/lib/src/services/intent_repository.dart b/lib/src/services/intent_repository.dart index 431b8bf3..9b5d4967 100644 --- a/lib/src/services/intent_repository.dart +++ b/lib/src/services/intent_repository.dart @@ -1,11 +1,11 @@ part of 'services.dart'; class IntentRepository { - // Singleton Pattern - static final IntentRepository _intentRepository = IntentRepository._(); factory IntentRepository() => _intentRepository; IntentRepository._(); + // Singleton Pattern + static final IntentRepository _intentRepository = IntentRepository._(); static final _navigationKey = GlobalKey(); static const platform = MethodChannel('app.channel.shared.data'); @@ -13,16 +13,14 @@ class IntentRepository { Future handleIntent() async { final importUrl = await platform.invokeMethod('getImportUrl') as String?; if (importUrl != null) { - _navigationKey.currentState?.pushAndRemoveUntil( + await _navigationKey.currentState?.pushAndRemoveUntil( MaterialPageRoute( - builder: (context) => RecipeImportScreen(importUrl), + builder: (context) => RecipeImportScreen(importUrl: importUrl), ), ModalRoute.withName('/'), ); } } - GlobalKey getNavigationKey() { - return _navigationKey; - } + GlobalKey getNavigationKey() => _navigationKey; } diff --git a/lib/src/services/net/nextcloud_metadata_api.dart b/lib/src/services/net/nextcloud_metadata_api.dart index 85795b3f..f34bfb31 100644 --- a/lib/src/services/net/nextcloud_metadata_api.dart +++ b/lib/src/services/net/nextcloud_metadata_api.dart @@ -1,17 +1,13 @@ part of '../services.dart'; class NextcloudMetadataApi { - final AppAuthentication _appAuthentication; - - factory NextcloudMetadataApi() { - return NextcloudMetadataApi._( - UserRepository().currentAppAuthentication, - ); - } + factory NextcloudMetadataApi() => NextcloudMetadataApi._( + UserRepository().currentAppAuthentication, + ); NextcloudMetadataApi._(this._appAuthentication); + final AppAuthentication _appAuthentication; - String getUserAvatarUrl() { - return "${_appAuthentication.server}/avatar/${_appAuthentication.loginName}/80"; - } + String getUserAvatarUrl() => + '${_appAuthentication.server}/avatar/${_appAuthentication.loginName}/80'; } diff --git a/lib/src/services/notification_provider.dart b/lib/src/services/notification_provider.dart index e71a96c9..2d8998e0 100644 --- a/lib/src/services/notification_provider.dart +++ b/lib/src/services/notification_provider.dart @@ -17,13 +17,12 @@ final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); class NotificationService { - static final NotificationService _notificationService = - NotificationService._(); - int curId = 0; - factory NotificationService() => _notificationService; NotificationService._(); + static final NotificationService _notificationService = + NotificationService._(); + int curId = 0; Future init() async { // initialize Timezone Database @@ -32,7 +31,7 @@ class NotificationService { tz.getLocation(await FlutterNativeTimezone.getLocalTimezone()), ); - const AndroidInitializationSettings initializationSettingsAndroid = + const initializationSettingsAndroid = AndroidInitializationSettings('notification_icon'); Future onDidReceiveLocalNotification( @@ -60,13 +59,11 @@ class NotificationService { ); */ } - final DarwinInitializationSettings initializationSettingsIOS = - DarwinInitializationSettings( + final initializationSettingsIOS = DarwinInitializationSettings( onDidReceiveLocalNotification: onDidReceiveLocalNotification, ); - final InitializationSettings initializationSettings = - InitializationSettings( + final initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, ); @@ -89,15 +86,17 @@ class NotificationService { final data = jsonDecode(element.payload!) as Map; final timer = Timer.fromJson(data)..id = element.id; TimerList()._timers.add(timer); - if (timer.id! > curId) curId = timer.id!; + if (timer.id! > curId) { + curId = timer.id!; + } } } } - void start(Timer timer) { + Future start(Timer timer) async { timer.id = curId; - _localNotifications.zonedSchedule( + await _localNotifications.zonedSchedule( curId++, timer.title, timer.body, @@ -110,15 +109,15 @@ class NotificationService { ); } - void cancel(Timer timer) { + Future cancel(Timer timer) async { assert( timer.id != null, "The timer should have an ID. If not it probably wasn't started", ); - _localNotifications.cancel(timer.id!); + await _localNotifications.cancel(timer.id!); } - void cancelAll() { - _localNotifications.cancelAll(); + Future cancelAll() async { + await _localNotifications.cancelAll(); } } diff --git a/lib/src/services/services.dart b/lib/src/services/services.dart index af979bdc..82830441 100644 --- a/lib/src/services/services.dart +++ b/lib/src/services/services.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:developer'; @@ -20,11 +21,11 @@ import 'package:timezone/data/latest_10y.dart' as tz; import 'package:timezone/timezone.dart' as tz; import 'package:xml/xml.dart'; -part "api_provider.dart"; -part "authentication_provider.dart"; -part "data_repository.dart"; -part "intent_repository.dart"; -part "net/nextcloud_metadata_api.dart"; -part "notification_provider.dart"; -part "user_repository.dart"; +part 'api_provider.dart'; +part 'authentication_provider.dart'; +part 'data_repository.dart'; +part 'intent_repository.dart'; +part 'net/nextcloud_metadata_api.dart'; +part 'notification_provider.dart'; +part 'user_repository.dart'; part 'timer_repository.dart'; diff --git a/lib/src/services/timer_repository.dart b/lib/src/services/timer_repository.dart index 5c879721..f38ac067 100644 --- a/lib/src/services/timer_repository.dart +++ b/lib/src/services/timer_repository.dart @@ -1,22 +1,21 @@ part of 'services.dart'; class TimerList { - static final TimerList _instance = TimerList._(); - final List _timers; - factory TimerList() => _instance; TimerList._() : _timers = []; + static final TimerList _instance = TimerList._(); + final List _timers; List get timers => _timers; void add(Timer timer) { - NotificationService().start(timer); + unawaited(NotificationService().start(timer)); _timers.add(timer); } void remove(Timer timer) { - NotificationService().cancel(timer); + unawaited(NotificationService().cancel(timer)); _timers.remove(timer); } } diff --git a/lib/src/services/user_repository.dart b/lib/src/services/user_repository.dart index 95b96802..c2158533 100644 --- a/lib/src/services/user_repository.dart +++ b/lib/src/services/user_repository.dart @@ -1,11 +1,11 @@ part of 'services.dart'; class UserRepository { - // Singleton - static final UserRepository _userRepository = UserRepository._(); factory UserRepository() => _userRepository; UserRepository._(); + // Singleton + static final UserRepository _userRepository = UserRepository._(); AuthenticationProvider authenticationProvider = AuthenticationProvider(); @@ -14,70 +14,60 @@ class UserRepository { String username, String originalBasicAuth, { required bool isSelfSignedCertificate, - }) async { - return authenticationProvider.authenticate( - serverUrl: serverUrl, - username: username, - originalBasicAuth: originalBasicAuth, - isSelfSignedCertificate: isSelfSignedCertificate, - ); - } + }) async => + authenticationProvider.authenticate( + serverUrl: serverUrl, + username: username, + originalBasicAuth: originalBasicAuth, + isSelfSignedCertificate: isSelfSignedCertificate, + ); Future authenticateAppPassword( String serverUrl, String username, String basicAuth, { required bool isSelfSignedCertificate, - }) async { - return authenticationProvider.authenticateAppPassword( - serverUrl: serverUrl, - username: username, - basicAuth: basicAuth, - isSelfSignedCertificate: isSelfSignedCertificate, - ); - } + }) async => + authenticationProvider.authenticateAppPassword( + serverUrl: serverUrl, + username: username, + basicAuth: basicAuth, + isSelfSignedCertificate: isSelfSignedCertificate, + ); void stopAuthenticate() { authenticationProvider.stopAuthenticate(); } - AppAuthentication get currentAppAuthentication { - return authenticationProvider.currentAppAuthentication!; - } + AppAuthentication get currentAppAuthentication => + authenticationProvider.currentAppAuthentication!; - Dio get authenticatedClient { - return currentAppAuthentication.authenticatedClient; - } + Dio get authenticatedClient => currentAppAuthentication.authenticatedClient; - Future hasAppAuthentication() async { - return authenticationProvider.hasAppAuthentication(); - } + Future hasAppAuthentication() async => + authenticationProvider.hasAppAuthentication(); - Future loadAppAuthentication() async { - return authenticationProvider.loadAppAuthentication(); - } + Future loadAppAuthentication() async => + authenticationProvider.loadAppAuthentication(); - Future checkAppAuthentication() async { - return authenticationProvider.checkAppAuthentication( - currentAppAuthentication.server, - currentAppAuthentication.basicAuth, - isSelfSignedCertificate: currentAppAuthentication.isSelfSignedCertificate, - ); - } + Future checkAppAuthentication() async => + authenticationProvider.checkAppAuthentication( + currentAppAuthentication.server, + currentAppAuthentication.basicAuth, + isSelfSignedCertificate: + currentAppAuthentication.isSelfSignedCertificate, + ); Future persistAppAuthentication( AppAuthentication appAuthentication, - ) async { - return authenticationProvider.persistAppAuthentication(appAuthentication); - } + ) async => + authenticationProvider.persistAppAuthentication(appAuthentication); - Future deleteAppAuthentication() async { - return authenticationProvider.deleteAppAuthentication(); - } + Future deleteAppAuthentication() async => + authenticationProvider.deleteAppAuthentication(); - bool isVersionSupported(APIVersion version) { - return ApiProvider().ncCookbookApi.isSupportedSync(version); - } + bool isVersionSupported(APIVersion version) => + ApiProvider().ncCookbookApi.isSupportedSync(version); Future fetchApiVersion() async { final response = await ApiProvider().miscApi.version(); diff --git a/lib/src/util/category_grid_delegate.dart b/lib/src/util/category_grid_delegate.dart index f092ba24..06864394 100644 --- a/lib/src/util/category_grid_delegate.dart +++ b/lib/src/util/category_grid_delegate.dart @@ -15,18 +15,18 @@ class CategoryGridDelegate extends SliverGridDelegate { @override SliverGridLayout getLayout(SliverConstraints constraints) { - int crossAxisCount = + var crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)) .ceil(); // Ensure a minimum count of 1, can be zero and result in an infinite extent // below when the window size is 0. crossAxisCount = math.max(1, crossAxisCount); final double usableCrossAxisExtent = math.max( - 0.0, + 0, constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1), ); - final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount; - final double childMainAxisExtent = childCrossAxisExtent + extent; + final childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount; + final childMainAxisExtent = childCrossAxisExtent + extent; return SliverGridRegularTileLayout( crossAxisCount: crossAxisCount, @@ -39,7 +39,6 @@ class CategoryGridDelegate extends SliverGridDelegate { } @override - bool shouldRelayout(CategoryGridDelegate oldDelegate) { - return oldDelegate.extent != extent; - } + bool shouldRelayout(CategoryGridDelegate oldDelegate) => + oldDelegate.extent != extent; } diff --git a/lib/src/util/custom_cache_manager.dart b/lib/src/util/custom_cache_manager.dart index 37a83adb..9c486001 100644 --- a/lib/src/util/custom_cache_manager.dart +++ b/lib/src/util/custom_cache_manager.dart @@ -5,10 +5,10 @@ import 'package:http/io_client.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; class CustomCacheManager { - static final CustomCacheManager _cacheManager = CustomCacheManager._(); factory CustomCacheManager() => _cacheManager; CustomCacheManager._(); + static final CustomCacheManager _cacheManager = CustomCacheManager._(); static const key = 'customCacheKey'; @@ -17,9 +17,7 @@ class CustomCacheManager { key, fileService: HttpFileService( httpClient: IOClient( - HttpClient() - ..badCertificateCallback = - (X509Certificate cert, String host, int port) => true, + HttpClient()..badCertificateCallback = (cert, host, port) => true, ), ), ), diff --git a/lib/src/util/duration_utils.dart b/lib/src/util/duration_utils.dart index 539da5a9..86d12d85 100644 --- a/lib/src/util/duration_utils.dart +++ b/lib/src/util/duration_utils.dart @@ -1,15 +1,12 @@ import 'package:flutter_translate/flutter_translate.dart'; extension DurationExtension on Duration { - String get translatedString { - return "$inHours ${translate('recipe.fields.time.hours')} : ${inMinutes.remainder(60)} ${translate('recipe.fields.time.minutes')}"; - } + String get translatedString => + "$inHours ${translate('recipe.fields.time.hours')} : ${inMinutes.remainder(60)} ${translate('recipe.fields.time.minutes')}"; - String formatMinutes() { - return "$inHours:${inMinutes.remainder(60).toString().padLeft(2, '0')}"; - } + String formatMinutes() => + "$inHours:${inMinutes.remainder(60).toString().padLeft(2, '0')}"; - String formatSeconds() { - return "${inHours.toString().padLeft(2, '0')}:${inMinutes.remainder(60).toString().padLeft(2, '0')}:${(inSeconds.remainder(60)).toString().padLeft(2, '0')}"; - } + String formatSeconds() => + "${inHours.toString().padLeft(2, '0')}:${inMinutes.remainder(60).toString().padLeft(2, '0')}:${(inSeconds.remainder(60)).toString().padLeft(2, '0')}"; } diff --git a/lib/src/util/lifecycle_event_handler.dart b/lib/src/util/lifecycle_event_handler.dart index b6cf8666..ba48a41b 100644 --- a/lib/src/util/lifecycle_event_handler.dart +++ b/lib/src/util/lifecycle_event_handler.dart @@ -2,13 +2,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; class LifecycleEventHandler extends WidgetsBindingObserver { - final AsyncCallback resumeCallBack; - final AsyncCallback? suspendingCallBack; - LifecycleEventHandler({ required this.resumeCallBack, this.suspendingCallBack, }); + final AsyncCallback resumeCallBack; + final AsyncCallback? suspendingCallBack; @override Future didChangeAppLifecycleState(AppLifecycleState state) async { diff --git a/lib/src/util/setting_keys.dart b/lib/src/util/setting_keys.dart index 683625a0..b556ef9f 100644 --- a/lib/src/util/setting_keys.dart +++ b/lib/src/util/setting_keys.dart @@ -4,8 +4,8 @@ enum SettingKeys { dark_mode, language, stay_awake, - @Deprecated("The font size will try to scale propperly") + @Deprecated('The font size will try to scale propperly') recipe_font_size, - @Deprecated("The font size will try to scale propperly") + @Deprecated('The font size will try to scale propperly') category_font_size, } diff --git a/lib/src/util/theme_data.dart b/lib/src/util/theme_data.dart index ccaeeb63..38c34a6f 100644 --- a/lib/src/util/theme_data.dart +++ b/lib/src/util/theme_data.dart @@ -29,7 +29,7 @@ class AppTheme { ], ); - static const nextCloudBlue = Color.fromRGBO(00, 130, 201, 1.0); + static const nextCloudBlue = Color.fromRGBO(00, 130, 201, 1); static final lightColorSheme = ColorScheme.fromSeed( seedColor: nextCloudBlue, @@ -73,11 +73,9 @@ class SnackBarThemes extends ThemeExtension { ); @override - SnackBarThemes copyWith({ColorScheme? colorScheme}) { - return SnackBarThemes( - colorScheme: colorScheme ?? this.colorScheme, - ); - } + SnackBarThemes copyWith({ColorScheme? colorScheme}) => SnackBarThemes( + colorScheme: colorScheme ?? this.colorScheme, + ); @override SnackBarThemes lerp(SnackBarThemes? other, double t) { diff --git a/lib/src/util/theme_mode_manager.dart b/lib/src/util/theme_mode_manager.dart index 01b13801..4bb1aec4 100644 --- a/lib/src/util/theme_mode_manager.dart +++ b/lib/src/util/theme_mode_manager.dart @@ -5,17 +5,13 @@ import 'package:theme_mode_handler/theme_mode_manager_interface.dart'; class ThemeModeManager implements IThemeModeManager { @override - Future loadThemeMode() { - return Future.value( - Settings.getValue( - SettingKeys.dark_mode.name, - defaultValue: ThemeMode.system.toString(), - ), - ); - } + Future loadThemeMode() => Future.value( + Settings.getValue( + SettingKeys.dark_mode.name, + defaultValue: ThemeMode.system.toString(), + ), + ); @override - Future saveThemeMode(String value) async { - return Future.value(true); - } + Future saveThemeMode(String value) async => Future.value(true); } diff --git a/lib/src/util/translate_preferences.dart b/lib/src/util/translate_preferences.dart index 863aee48..f1bf9dee 100644 --- a/lib/src/util/translate_preferences.dart +++ b/lib/src/util/translate_preferences.dart @@ -19,7 +19,5 @@ class TranslatePreferences implements ITranslatePreferences { } @override - Future savePreferredLocale(Locale locale) { - return Future.value(true); - } + Future savePreferredLocale(Locale locale) => Future.value(true); } diff --git a/lib/src/util/url_validator.dart b/lib/src/util/url_validator.dart index bbcaf8e3..869d4930 100644 --- a/lib/src/util/url_validator.dart +++ b/lib/src/util/url_validator.dart @@ -24,25 +24,25 @@ class URLUtils { /// 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://"; + var prefix = ''; + var 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); + const pattern = r'(?:\.|^)([^.]*?[^\x00-\x7F][^.]*?)(?:\.|$)'; + final expression = RegExp(pattern, caseSensitive: false); final matches = expression.allMatches(punycodeUrl); for (final exp in matches) { - final String match = exp.group(1)!; + final match = exp.group(1)!; punycodeUrl = - punycodeUrl.replaceFirst(match, "xn--${punycodeEncode(match)}"); + punycodeUrl.replaceFirst(match, 'xn--${punycodeEncode(match)}'); } return prefix + punycodeUrl; @@ -68,15 +68,15 @@ class URLUtils { static String sanitizeUrl(String url) { if (!isValid(url)) { throw const FormatException( - "given url is not valid. Please validate first with URLUtils.isValid(url)", + 'given url is not valid. Please validate first with URLUtils.isValid(url)', ); } - String encodedUrl = _punyEncodeUrl(url); + var encodedUrl = _punyEncodeUrl(url); if (encodedUrl.substring(0, 4) != 'http') { encodedUrl = 'https://$encodedUrl'; } - if (encodedUrl.endsWith("/")) { + if (encodedUrl.endsWith('/')) { encodedUrl = encodedUrl.substring(0, encodedUrl.length - 1); } diff --git a/lib/src/util/wakelock.dart b/lib/src/util/wakelock.dart index 7883b8b7..6027a5a9 100644 --- a/lib/src/util/wakelock.dart +++ b/lib/src/util/wakelock.dart @@ -2,19 +2,17 @@ import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; import 'package:wakelock/wakelock.dart'; -Future disableWakelock() async { - final bool wakelockEnabled = await Wakelock.enabled; - if (wakelockEnabled) { - Wakelock.disable(); - } - return Future.value(true); +Future disableWakelock() async { + await Wakelock.disable(); } -void enableWakelock() { - if (Settings.getValue( +Future enableWakelock() async { + final enable = Settings.getValue( SettingKeys.stay_awake.name, defaultValue: false, - )!) { - Wakelock.enable(); + )!; + + if (enable) { + await Wakelock.enable(); } } diff --git a/lib/src/widget/alerts/recipe_delete_alert.dart b/lib/src/widget/alerts/recipe_delete_alert.dart index d699719d..a31294e7 100644 --- a/lib/src/widget/alerts/recipe_delete_alert.dart +++ b/lib/src/widget/alerts/recipe_delete_alert.dart @@ -11,27 +11,25 @@ class DeleteRecipeAlert extends StatelessWidget { final Recipe recipe; @override - Widget build(BuildContext context) { - return AlertDialog( - icon: const Icon(Icons.delete_forever), - iconColor: Theme.of(context).colorScheme.error, - title: Text(translate("recipe_edit.delete.title")), - content: Text( - translate( - "recipe_edit.delete.dialog", - args: {"recipe": recipe.name}, + Widget build(BuildContext context) => AlertDialog( + icon: const Icon(Icons.delete_forever), + iconColor: Theme.of(context).colorScheme.error, + title: Text(translate('recipe_edit.delete.title')), + content: Text( + translate( + 'recipe_edit.delete.dialog', + args: {'recipe': recipe.name}, + ), ), - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(MaterialLocalizations.of(context).deleteButtonTooltip), - ), - ], - ); - } + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(MaterialLocalizations.of(context).deleteButtonTooltip), + ), + ], + ); } diff --git a/lib/src/widget/alerts/recipe_edit_alert.dart b/lib/src/widget/alerts/recipe_edit_alert.dart index a53a259f..485ce724 100644 --- a/lib/src/widget/alerts/recipe_edit_alert.dart +++ b/lib/src/widget/alerts/recipe_edit_alert.dart @@ -18,7 +18,7 @@ class CancelEditAlert extends StatelessWidget { content: Text( translate( 'recipe_form.$key.dialog', - args: {"recipe": recipe?.name}, + args: {'recipe': recipe?.name}, ), ), actions: [ diff --git a/lib/src/widget/animated_time_progress_bar.dart b/lib/src/widget/animated_time_progress_bar.dart index 76da38ee..53b207ac 100644 --- a/lib/src/widget/animated_time_progress_bar.dart +++ b/lib/src/widget/animated_time_progress_bar.dart @@ -4,47 +4,44 @@ import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; import 'package:nextcloud_cookbook_flutter/src/util/duration_utils.dart'; class AnimatedTimeProgressBar extends StatelessWidget { - final Timer timer; - const AnimatedTimeProgressBar({ - super.key, required this.timer, + super.key, }); + final Timer timer; @override - Widget build(BuildContext context) { - return TweenAnimationBuilder( - duration: timer.remaining, - tween: Tween( - begin: timer.progress, - end: 1.0, - ), - builder: (context, value, child) { - if (value == 1.0) { - return Text(translate('timer.done')); - } + Widget build(BuildContext context) => TweenAnimationBuilder( + duration: timer.remaining, + tween: Tween( + begin: timer.progress, + end: 1, + ), + builder: (context, value, child) { + if (value == 1.0) { + return Text(translate('timer.done')); + } - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(timer.remaining.formatSeconds()), - child!, - ], - ), - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: LinearProgressIndicator( - value: value, - color: Theme.of(context).colorScheme.primaryContainer, - semanticsLabel: timer.title, + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(timer.remaining.formatSeconds()), + child!, + ], + ), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: value, + color: Theme.of(context).colorScheme.primaryContainer, + semanticsLabel: timer.title, + ), ), - ), - ], - ); - }, - child: Text(timer.duration.formatSeconds()), - ); - } + ], + ); + }, + child: Text(timer.duration.formatSeconds()), + ); } diff --git a/lib/src/widget/category_card.dart b/lib/src/widget/category_card.dart index 56018b48..d0499498 100644 --- a/lib/src/widget/category_card.dart +++ b/lib/src/widget/category_card.dart @@ -5,17 +5,16 @@ import 'package:nextcloud_cookbook_flutter/src/screens/recipes_list_screen.dart' import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; class CategoryCard extends StatelessWidget { - final Category category; - final String? imageID; - const CategoryCard( this.category, this.imageID, { super.key, }); + final Category category; + final String? imageID; static const double _spacer = 8; - static const _labelPadding = EdgeInsets.symmetric(horizontal: 8.0); + static const _labelPadding = EdgeInsets.symmetric(horizontal: 8); static TextStyle _nameStyle(BuildContext context) => Theme.of(context).textTheme.labelSmall!; @@ -23,64 +22,62 @@ class CategoryCard extends StatelessWidget { static TextStyle _itemStyle(BuildContext context) => Theme.of(context).textTheme.labelSmall!; - static double hightExtend(BuildContext context) { - return _spacer + - _itemStyle(context).fontSize! + - _itemStyle(context).fontSize! + - 2 * _labelPadding.horizontal; - } + static double hightExtend(BuildContext context) => + _spacer + + _itemStyle(context).fontSize! + + _itemStyle(context).fontSize! + + 2 * _labelPadding.horizontal; @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final size = constraints.maxWidth; + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) { + final size = constraints.maxWidth; - final String itemsText = translatePlural( - 'categories.items', - category.recipeCount, - ); + final itemsText = translatePlural( + 'categories.items', + category.recipeCount, + ); - return GestureDetector( - child: Card( - color: Theme.of(context).colorScheme.secondaryContainer, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: RecipeImage( - id: imageID, - size: Size.square(size), + return GestureDetector( + child: Card( + color: Theme.of(context).colorScheme.secondaryContainer, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: RecipeImage( + id: imageID, + size: Size.square(size), + ), ), - ), - const SizedBox(height: _spacer), - Padding( - padding: _labelPadding, - child: Text( - category.name, - maxLines: 1, - style: _nameStyle(context), + const SizedBox(height: _spacer), + Padding( + padding: _labelPadding, + child: Text( + category.name, + maxLines: 1, + style: _nameStyle(context), + ), ), - ), - Padding( - padding: _labelPadding, - child: Text( - itemsText, - style: _itemStyle(context), + Padding( + padding: _labelPadding, + child: Text( + itemsText, + style: _itemStyle(context), + ), ), - ), - ], + ], + ), ), - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RecipesListScreen(category: category.name), + onTap: () async => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + RecipesListScreen(category: category.name), + ), ), - ), - ); - }, - ); - } + ); + }, + ); } diff --git a/lib/src/widget/checkbox_form_field.dart b/lib/src/widget/checkbox_form_field.dart index a494a489..c59bca3c 100644 --- a/lib/src/widget/checkbox_form_field.dart +++ b/lib/src/widget/checkbox_form_field.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; class CheckboxFormField extends FormField { CheckboxFormField({ + super.key, Widget? title, super.onSaved, super.validator, @@ -9,24 +10,22 @@ class CheckboxFormField extends FormField { AutovalidateMode autoValidateMode = AutovalidateMode.disabled, }) : super( autovalidateMode: autoValidateMode, - builder: (FormFieldState state) { - return CheckboxListTile( - dense: state.hasError, - title: title, - value: state.value, - onChanged: state.didChange, - subtitle: state.hasError - ? Builder( - builder: (BuildContext context) => Text( - state.errorText!, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), + builder: (state) => CheckboxListTile( + dense: state.hasError, + title: title, + value: state.value, + onChanged: state.didChange, + subtitle: state.hasError + ? Builder( + builder: (context) => Text( + state.errorText!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, ), - ) - : null, - controlAffinity: ListTileControlAffinity.leading, - ); - }, + ), + ) + : null, + controlAffinity: ListTileControlAffinity.leading, + ), ); } diff --git a/lib/src/widget/drawer.dart b/lib/src/widget/drawer.dart index fda23456..1a0bfbc2 100644 --- a/lib/src/widget/drawer.dart +++ b/lib/src/widget/drawer.dart @@ -14,65 +14,63 @@ class MainDrawer extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - DrawerHeader( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, + Widget build(BuildContext context) => Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + ), + child: const UserImage(), ), - child: const UserImage(), - ), - DrawerItem( - icon: Icons.alarm_add_outlined, - title: translate('timer.title'), - onTap: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const TimerScreen(), - ), - ); - }, - ), - DrawerItem( - icon: Icons.cloud_download_outlined, - title: translate('categories.drawer.import'), - onTap: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const RecipeImportScreen(), - ), - ); - }, - ), - DrawerItem( - icon: Icons.settings_outlined, - title: translate('categories.drawer.settings'), - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const MySettingsScreen(), - ), - ); - }, - ), - DrawerItem( - icon: Icons.exit_to_app_outlined, - title: translate('app_bar.logout'), - onTap: () { - BlocProvider.of(context) - .add(const LoggedOut()); - }, - ), - ], - ), - ); - } + DrawerItem( + icon: Icons.alarm_add_outlined, + title: translate('timer.title'), + onTap: () async { + Navigator.pop(context); + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const TimerScreen(), + ), + ); + }, + ), + DrawerItem( + icon: Icons.cloud_download_outlined, + title: translate('categories.drawer.import'), + onTap: () async { + Navigator.pop(context); + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RecipeImportScreen(), + ), + ); + }, + ), + DrawerItem( + icon: Icons.settings_outlined, + title: translate('categories.drawer.settings'), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MySettingsScreen(), + ), + ); + }, + ), + DrawerItem( + icon: Icons.exit_to_app_outlined, + title: translate('app_bar.logout'), + onTap: () { + BlocProvider.of(context) + .add(const LoggedOut()); + }, + ), + ], + ), + ); } diff --git a/lib/src/widget/input/duration_form_field.dart b/lib/src/widget/input/duration_form_field.dart index 5398f4ef..d8e73343 100644 --- a/lib/src/widget/input/duration_form_field.dart +++ b/lib/src/widget/input/duration_form_field.dart @@ -8,13 +8,6 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/util/duration_utils.dart'; class DurationFormField extends StatefulWidget { - final bool? enabled; - final Duration? initialValue; - final InputDecoration? decoration; - final void Function(Duration? value)? onSaved; - final TextAlign textAlign; - final FocusNode? focusNode; - const DurationFormField({ super.key, this.enabled, @@ -24,6 +17,12 @@ class DurationFormField extends StatefulWidget { this.focusNode, this.textAlign = TextAlign.end, }); + final bool? enabled; + final Duration? initialValue; + final InputDecoration? decoration; + final void Function(Duration? value)? onSaved; + final TextAlign textAlign; + final FocusNode? focusNode; @override _DurationFormFieldState createState() => _DurationFormFieldState(); @@ -71,23 +70,21 @@ class _DurationFormFieldState extends State { } @override - Widget build(BuildContext context) { - return TextFormField( - controller: controller, - readOnly: true, - showCursor: false, - enabled: widget.enabled, - onTap: onTap, - focusNode: focusNode, - decoration: widget.decoration, - textAlign: widget.textAlign, - onSaved: (value) { - if (value != null && value.isNotEmpty) { - widget.onSaved?.call(duration); - } - }, - ); - } + Widget build(BuildContext context) => TextFormField( + controller: controller, + readOnly: true, + showCursor: false, + enabled: widget.enabled, + onTap: onTap, + focusNode: focusNode, + decoration: widget.decoration, + textAlign: widget.textAlign, + onSaved: (value) { + if (value != null && value.isNotEmpty) { + widget.onSaved?.call(duration); + } + }, + ); } const Duration _kDialogSizeAnimationDuration = Duration(milliseconds: 200); @@ -95,14 +92,13 @@ const Duration _kVibrateCommitDelay = Duration(milliseconds: 100); enum _DurationPickerMode { hour, minute } -const double _kDurationPickerHeaderControlHeight = 80.0; +const double _kDurationPickerHeaderControlHeight = 80; -const double _kDurationPickerWidthPortrait = 328.0; +const double _kDurationPickerWidthPortrait = 328; -const double _kDurationPickerHeightInput = 226.0; +const double _kDurationPickerHeightInput = 226; -const BorderRadius _kDefaultBorderRadius = - BorderRadius.all(Radius.circular(4.0)); +const BorderRadius _kDefaultBorderRadius = BorderRadius.all(Radius.circular(4)); const ShapeBorder _kDefaultShape = RoundedRectangleBorder(borderRadius: _kDefaultBorderRadius); @@ -128,7 +124,7 @@ class RestorableDuration extends RestorableValue { @override Duration fromPrimitives(Object? data) { - final List timeData = data! as List; + final timeData = data! as List; return Duration( minutes: timeData[0]! as int, hours: value.inHours, @@ -145,26 +141,26 @@ class _StringFragment extends StatelessWidget { @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); - final TimePickerThemeData durationPickerTheme = TimePickerTheme.of(context); - final TextStyle hourMinuteStyle = durationPickerTheme.hourMinuteTextStyle ?? + final theme = Theme.of(context); + final durationPickerTheme = TimePickerTheme.of(context); + final hourMinuteStyle = durationPickerTheme.hourMinuteTextStyle ?? theme.textTheme.displayMedium!; - final Color textColor = + final textColor = durationPickerTheme.hourMinuteTextColor ?? theme.colorScheme.onSurface; return ExcludeSemantics( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), + padding: const EdgeInsets.symmetric(horizontal: 6), child: Center( child: Text( - ":", + ':', style: hourMinuteStyle.apply( color: MaterialStateProperty.resolveAs( textColor, {}, ), ), - textScaleFactor: 1.0, + textScaleFactor: 1, ), ), ), @@ -241,7 +237,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> return null; } - final int? newHour = int.tryParse(value); + final newHour = int.tryParse(value); if (newHour == null) { return null; } @@ -258,7 +254,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> return null; } - final int? newMinute = int.tryParse(value); + final newMinute = int.tryParse(value); if (newMinute == null) { return null; } @@ -270,7 +266,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> } void _handleHourSavedSubmitted(String? value) { - final int? newHour = _parseHour(value); + final newHour = _parseHour(value); if (newHour != null) { _selectedTime.value = Duration( hours: newHour, @@ -281,7 +277,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> } void _handleHourChanged(String value) { - final int? newHour = _parseHour(value); + final newHour = _parseHour(value); if (newHour != null && value.length == 2) { // If a valid hour is typed, move focus to the minute TextField. FocusScope.of(context).nextFocus(); @@ -289,7 +285,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> } void _handleMinuteSavedSubmitted(String? value) { - final int? newMinute = _parseMinute(value); + final newMinute = _parseMinute(value); if (newMinute != null) { _selectedTime.value = Duration( hours: _selectedTime.value.inHours, @@ -300,7 +296,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> } String? _validateHour(String? value) { - final int? newHour = _parseHour(value); + final newHour = _parseHour(value); setState(() { hourHasError.value = newHour == null; }); @@ -311,7 +307,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> } String? _validateMinute(String? value) { - final int? newMinute = _parseMinute(value); + final newMinute = _parseMinute(value); setState(() { minuteHasError.value = newMinute == null; }); @@ -324,13 +320,12 @@ class _DurationPickerInputState extends State<_DurationPickerInput> @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); - final ThemeData theme = Theme.of(context); - final TextStyle hourMinuteStyle = - TimePickerTheme.of(context).hourMinuteTextStyle ?? - theme.textTheme.displayMedium!; + final theme = Theme.of(context); + final hourMinuteStyle = TimePickerTheme.of(context).hourMinuteTextStyle ?? + theme.textTheme.displayMedium!; return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -340,7 +335,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> style: TimePickerTheme.of(context).helpTextStyle ?? theme.textTheme.labelSmall, ), - const SizedBox(height: 16.0), + const SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, // Hour/minutes should not change positions in RTL locales. @@ -350,7 +345,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 8.0), + const SizedBox(height: 8), _HourTextField( restorationId: 'hour_text_field', selectedTime: _selectedTime.value, @@ -361,7 +356,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> onChanged: _handleHourChanged, hourLabelText: widget.hourLabelText, ), - const SizedBox(height: 8.0), + const SizedBox(height: 8), if (!hourHasError.value && !minuteHasError.value) ExcludeSemantics( child: Text( @@ -377,7 +372,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> ), ), Container( - margin: const EdgeInsets.only(top: 8.0), + margin: const EdgeInsets.only(top: 8), height: _kDurationPickerHeaderControlHeight, child: const _StringFragment(), ), @@ -385,7 +380,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 8.0), + const SizedBox(height: 8), _MinuteTextField( restorationId: 'minute_text_field', selectedTime: _selectedTime.value, @@ -395,7 +390,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> onSavedSubmitted: _handleMinuteSavedSubmitted, minuteLabelText: widget.minuteLabelText, ), - const SizedBox(height: 8.0), + const SizedBox(height: 8), if (!hourHasError.value && !minuteHasError.value) ExcludeSemantics( child: Text( @@ -420,7 +415,7 @@ class _DurationPickerInputState extends State<_DurationPickerInput> .copyWith(color: theme.colorScheme.error), ) else - const SizedBox(height: 2.0), + const SizedBox(height: 2), ], ), ); @@ -449,20 +444,18 @@ class _HourTextField extends StatelessWidget { final String? restorationId; @override - Widget build(BuildContext context) { - return _HourMinuteTextField( - restorationId: restorationId, - selectedTime: selectedTime, - isHour: true, - autofocus: autofocus, - style: style, - semanticHintText: hourLabelText ?? - MaterialLocalizations.of(context).timePickerHourLabel, - validator: validator, - onSavedSubmitted: onSavedSubmitted, - onChanged: onChanged, - ); - } + Widget build(BuildContext context) => _HourMinuteTextField( + restorationId: restorationId, + selectedTime: selectedTime, + isHour: true, + autofocus: autofocus, + style: style, + semanticHintText: hourLabelText ?? + MaterialLocalizations.of(context).timePickerHourLabel, + validator: validator, + onSavedSubmitted: onSavedSubmitted, + onChanged: onChanged, + ); } class _MinuteTextField extends StatelessWidget { @@ -485,19 +478,17 @@ class _MinuteTextField extends StatelessWidget { final String? restorationId; @override - Widget build(BuildContext context) { - return _HourMinuteTextField( - restorationId: restorationId, - selectedTime: selectedTime, - isHour: false, - autofocus: autofocus, - style: style, - semanticHintText: minuteLabelText ?? - MaterialLocalizations.of(context).timePickerMinuteLabel, - validator: validator, - onSavedSubmitted: onSavedSubmitted, - ); - } + Widget build(BuildContext context) => _HourMinuteTextField( + restorationId: restorationId, + selectedTime: selectedTime, + isHour: false, + autofocus: autofocus, + style: style, + semanticHintText: minuteLabelText ?? + MaterialLocalizations.of(context).timePickerMinuteLabel, + validator: validator, + onSavedSubmitted: onSavedSubmitted, + ); } class _HourMinuteTextField extends StatefulWidget { @@ -570,20 +561,17 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> registerForRestoration(controllerHasBeenSet, 'has_controller_been_set'); } - String get _formattedValue { - return !widget.isHour - ? widget.selectedTime.inMinutes.remainder(60).toString().padLeft(2, '0') - : widget.selectedTime.inHours.remainder(60).toString().padLeft(2, '0'); - } + String get _formattedValue => !widget.isHour + ? widget.selectedTime.inMinutes.remainder(60).toString().padLeft(2, '0') + : widget.selectedTime.inHours.remainder(60).toString().padLeft(2, '0'); @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); - final TimePickerThemeData durationPickerTheme = TimePickerTheme.of(context); - final ColorScheme colorScheme = theme.colorScheme; + final theme = Theme.of(context); + final durationPickerTheme = TimePickerTheme.of(context); + final colorScheme = theme.colorScheme; - final InputDecorationTheme? inputDecorationTheme = - durationPickerTheme.inputDecorationTheme; + final inputDecorationTheme = durationPickerTheme.inputDecorationTheme; InputDecoration inputDecoration; if (inputDecorationTheme != null) { inputDecoration = @@ -596,24 +584,24 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> borderSide: BorderSide(color: Colors.transparent), ), errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: colorScheme.error, width: 2.0), + borderSide: BorderSide(color: colorScheme.error, width: 2), ), focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: colorScheme.primary, width: 2.0), + borderSide: BorderSide(color: colorScheme.primary, width: 2), ), focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide(color: colorScheme.error, width: 2.0), + borderSide: BorderSide(color: colorScheme.error, width: 2), ), hintStyle: widget.style .copyWith(color: colorScheme.onSurface.withOpacity(0.36)), // TODO(rami-a): Remove this logic once https://github.com/flutter/flutter/issues/54104 is fixed. errorStyle: const TextStyle( - fontSize: 0.0, - height: 0.0, + fontSize: 0, + height: 0, ), // Prevent the error text from appearing. ); } - final Color unfocusedFillColor = durationPickerTheme.hourMinuteColor ?? + final unfocusedFillColor = durationPickerTheme.hourMinuteColor ?? colorScheme.onSurface.withOpacity(0.12); // If screen reader is in use, make the hint text say hours/minutes. // Otherwise, remove the hint text when focused because the centered cursor @@ -621,7 +609,7 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> // // TODO(rami-a): Once https://github.com/flutter/flutter/issues/67571 is // resolved, remove the window check for semantics being enabled on web. - final String? hintText = MediaQuery.of(context).accessibleNavigation || + final hintText = MediaQuery.of(context).accessibleNavigation || WidgetsBinding.instance.window.semanticsEnabled ? widget.semanticHintText : (focusNode.hasFocus ? null : _formattedValue); @@ -635,7 +623,7 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> return SizedBox( height: _kDurationPickerHeaderControlHeight, child: MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + data: MediaQuery.of(context).copyWith(textScaleFactor: 1), child: UnmanagedRestorationScope( bucket: bucket, child: TextFormField( @@ -864,7 +852,7 @@ class _DurationPickerDialogState extends State case TargetPlatform.windows: _vibrateTimer?.cancel(); _vibrateTimer = Timer(_kVibrateCommitDelay, () { - HapticFeedback.vibrate(); + unawaited(HapticFeedback.vibrate()); _vibrateTimer = null; }); break; @@ -921,7 +909,7 @@ class _DurationPickerDialogState extends State } void _handleOk() { - final FormState form = _formKey.currentState!; + final form = _formKey.currentState!; if (!form.validate()) { setState(() { _autovalidateMode.value = AutovalidateMode.always; @@ -949,17 +937,16 @@ class _DurationPickerDialogState extends State @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); - final ThemeData theme = Theme.of(context); - final ShapeBorder shape = - TimePickerTheme.of(context).shape ?? _kDefaultShape; + final theme = Theme.of(context); + final shape = TimePickerTheme.of(context).shape ?? _kDefaultShape; final Widget actions = Row( children: [ - const SizedBox(width: 10.0), + const SizedBox(width: 10), Expanded( child: Container( alignment: AlignmentDirectional.centerEnd, - constraints: const BoxConstraints(minHeight: 52.0), + constraints: const BoxConstraints(minHeight: 52), padding: const EdgeInsets.symmetric(horizontal: 8), child: OverflowBar( spacing: 8, @@ -1008,13 +995,13 @@ class _DurationPickerDialogState extends State ), ); - final Size dialogSize = _dialogSize(context); + final dialogSize = _dialogSize(context); return Dialog( shape: shape, backgroundColor: TimePickerTheme.of(context).backgroundColor ?? theme.colorScheme.surface, insetPadding: const EdgeInsets.symmetric( - horizontal: 16.0, + horizontal: 16, ), child: AnimatedContainer( width: dialogSize.width, @@ -1035,5 +1022,5 @@ class _DurationPickerDialogState extends State } void _announceToAccessibility(BuildContext context, String message) { - SemanticsService.announce(message, Directionality.of(context)); + unawaited(SemanticsService.announce(message, Directionality.of(context))); } diff --git a/lib/src/widget/input/integer_text_form_field.dart b/lib/src/widget/input/integer_text_form_field.dart index a97589d6..3e1e237a 100644 --- a/lib/src/widget/input/integer_text_form_field.dart +++ b/lib/src/widget/input/integer_text_form_field.dart @@ -2,16 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; class IntegerTextFormField extends StatelessWidget { - final int initialValue; - final bool? enabled; - final InputDecoration? decoration; - final ValueChanged? onSaved; - final ValueChanged? onChanged; - final TextInputAction? textInputAction; - final int? minValue; - final int? maxValue; - final TextAlign textAlign; - IntegerTextFormField({ super.key, int? initialValue, @@ -27,42 +17,57 @@ class IntegerTextFormField extends StatelessWidget { initialValue = initialValue ?? minValue!, assert(minValue == null || initialValue! >= minValue), assert((minValue == null || maxValue == null) || minValue <= maxValue); + final int initialValue; + final bool? enabled; + final InputDecoration? decoration; + final ValueChanged? onSaved; + final ValueChanged? onChanged; + final TextInputAction? textInputAction; + final int? minValue; + final int? maxValue; + final TextAlign textAlign; @override - Widget build(BuildContext context) { - return TextFormField( - enabled: enabled, - initialValue: initialValue.toString(), - decoration: decoration, - textAlign: textAlign, - keyboardType: TextInputType.number, - textInputAction: textInputAction, - onSaved: (newValue) { - if (newValue == null) return; + Widget build(BuildContext context) => TextFormField( + enabled: enabled, + initialValue: initialValue.toString(), + decoration: decoration, + textAlign: textAlign, + keyboardType: TextInputType.number, + textInputAction: textInputAction, + onSaved: (newValue) { + if (newValue == null) { + return; + } - onSaved?.call(int.parse(newValue)); - }, - onChanged: (value) { - final int$ = int.tryParse(value); - if (int$ != null) { - onChanged?.call(int$); - } - }, - validator: (value) { - if (value == null || !ensureMinMax(int.tryParse(value))) { - return translate("form.validators.invalid_number"); - } + onSaved?.call(int.parse(newValue)); + }, + onChanged: (value) { + final int$ = int.tryParse(value); + if (int$ != null) { + onChanged?.call(int$); + } + }, + validator: (value) { + if (value == null || !ensureMinMax(int.tryParse(value))) { + return translate('form.validators.invalid_number'); + } - return null; - }, - ); - } + return null; + }, + ); bool ensureMinMax(int? value) { - if (value == null) return false; + if (value == null) { + return false; + } - if (minValue != null && value < minValue!) return false; - if (maxValue != null && value > maxValue!) return false; + if (minValue != null && value < minValue!) { + return false; + } + if (maxValue != null && value > maxValue!) { + return false; + } return true; } diff --git a/lib/src/widget/input/reorderable_list_form_field.dart b/lib/src/widget/input/reorderable_list_form_field.dart index 067d27a3..5a597ebb 100644 --- a/lib/src/widget/input/reorderable_list_form_field.dart +++ b/lib/src/widget/input/reorderable_list_form_field.dart @@ -5,7 +5,6 @@ import 'package:sliver_tools/sliver_tools.dart'; class ReordarableListFormField extends FormField> { ReordarableListFormField({ - super.key, required String title, ListBuilder? initialValues, super.onSaved, @@ -14,36 +13,34 @@ class ReordarableListFormField extends FormField> { AutovalidateMode? autovalidateMode, InputDecoration decoration = const InputDecoration(), super.restorationId, + super.key, }) : super( initialValue: initialValues, autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled, - builder: (FormFieldState> state) { - return UnmanagedRestorationScope( - bucket: state.bucket, - child: ReordarableListField( - title: title, - items: initialValues?.build().toList(), - enabled: enabled, - decoration: decoration, - ), - ); - }, + builder: (state) => UnmanagedRestorationScope( + bucket: state.bucket, + child: ReordarableListField( + title: title, + items: initialValues?.build().toList(), + enabled: enabled, + decoration: decoration, + ), + ), ); } class ReordarableListField extends StatefulWidget { - final String title; - final List? items; - final bool enabled; - final InputDecoration decoration; - const ReordarableListField({ - super.key, required this.title, this.items, this.enabled = true, this.decoration = const InputDecoration(), + super.key, }); + final String title; + final List? items; + final bool enabled; + final InputDecoration decoration; @override _ReordarableListFieldState createState() => _ReordarableListFieldState(); @@ -81,17 +78,15 @@ class _ReordarableListFieldState extends State { }); } - Widget _buildItem(BuildContext context, int index) { - return Item( - key: UniqueKey(), - controller: _items[index], - index: index, - focus: index == _items.length - 1, - onDismissed: () { - setState(() => _items.removeAt(index)); - }, - ); - } + Widget _buildItem(BuildContext context, int index) => Item( + key: UniqueKey(), + controller: _items[index], + index: index, + focus: index == _items.length - 1, + onDismissed: () { + setState(() => _items.removeAt(index)); + }, + ); @override Widget build(BuildContext context) { @@ -108,7 +103,7 @@ class _ReordarableListFieldState extends State { ); final items = SliverPadding( - padding: const EdgeInsets.only(top: 8.0), + padding: const EdgeInsets.only(top: 8), sliver: SliverReorderableList( onReorder: _reorderCallback, itemCount: _items.length, @@ -121,7 +116,7 @@ class _ReordarableListFieldState extends State { focusNode: focusNode, icon: const Icon(Icons.add_outlined), label: Text( - translate("recipe_create.add_field"), + translate('recipe_create.add_field'), ), onPressed: () { if (widget.enabled) { @@ -142,14 +137,14 @@ class _ReordarableListFieldState extends State { class Item extends StatefulWidget { const Item({ - super.key, required VoidCallback this.onDismissed, required TextEditingController this.controller, required this.index, this.focus = false, + super.key, }); - const Item.prototype() + const Item.prototype({super.key}) : onDismissed = null, controller = null, index = 0, @@ -192,7 +187,7 @@ class _ItemState extends State { ); final deleteButton = IconButton( - tooltip: translate("recipe_create.remove_field"), + tooltip: translate('recipe_create.remove_field'), icon: Icon( Icons.delete, color: Theme.of(context).colorScheme.error, diff --git a/lib/src/widget/recipe/widget/duration_list.dart b/lib/src/widget/recipe/widget/duration_list.dart index d4dcfb7c..cec1e4c9 100644 --- a/lib/src/widget/recipe/widget/duration_list.dart +++ b/lib/src/widget/recipe/widget/duration_list.dart @@ -1,11 +1,11 @@ part of '../recipe_screen.dart'; class DurationList extends StatelessWidget { - final Recipe recipe; - const DurationList({ required this.recipe, + super.key, }); + final Recipe recipe; @override Widget build(BuildContext context) { diff --git a/lib/src/widget/recipe/widget/ingredient_list.dart b/lib/src/widget/recipe/widget/ingredient_list.dart index b0a1dc42..9ea3c190 100644 --- a/lib/src/widget/recipe/widget/ingredient_list.dart +++ b/lib/src/widget/recipe/widget/ingredient_list.dart @@ -1,30 +1,27 @@ part of '../recipe_screen.dart'; class IngredientList extends StatelessWidget { - final Recipe recipe; - const IngredientList( - this.recipe, - ); + this.recipe, { + super.key, + }); + final Recipe recipe; @override - Widget build(BuildContext context) { - return ExpansionTile( - childrenPadding: const EdgeInsets.symmetric(horizontal: 8.0), - title: Text(translate('recipe.fields.ingredients')), - initiallyExpanded: true, - children: [ - for (final ingredient in recipe.recipeIngredient) - _IngredientListItem(ingredient) - ], - ); - } + Widget build(BuildContext context) => ExpansionTile( + childrenPadding: const EdgeInsets.symmetric(horizontal: 8), + title: Text(translate('recipe.fields.ingredients')), + initiallyExpanded: true, + children: [ + for (final ingredient in recipe.recipeIngredient) + _IngredientListItem(ingredient) + ], + ); } class _IngredientListItem extends StatefulWidget { - final String ingredient; - const _IngredientListItem(this.ingredient); + final String ingredient; @override State<_IngredientListItem> createState() => __IngredientListItemState(); @@ -47,7 +44,7 @@ class __IngredientListItemState extends State<_IngredientListItem> { } return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), + padding: const EdgeInsets.symmetric(horizontal: 24), child: GestureDetector( onTap: () => setState(() { selected = !selected; diff --git a/lib/src/widget/recipe/widget/instruction_list.dart b/lib/src/widget/recipe/widget/instruction_list.dart index afc8c48e..a86d2057 100644 --- a/lib/src/widget/recipe/widget/instruction_list.dart +++ b/lib/src/widget/recipe/widget/instruction_list.dart @@ -1,11 +1,11 @@ part of '../recipe_screen.dart'; class InstructionList extends StatelessWidget { - final Recipe recipe; - const InstructionList( - this.recipe, - ); + this.recipe, { + super.key, + }); + final Recipe recipe; @override Widget build(BuildContext context) { @@ -22,11 +22,10 @@ class InstructionList extends StatelessWidget { } class _InstructionListTitem extends StatefulWidget { + const _InstructionListTitem(this.instruction, this.index); final String instruction; final int index; - const _InstructionListTitem(this.instruction, this.index); - @override State<_InstructionListTitem> createState() => _InstructionListTitemState(); } @@ -35,40 +34,38 @@ class _InstructionListTitemState extends State<_InstructionListTitem> { bool selected = false; @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 5), - child: GestureDetector( - onTap: () => setState(() { - selected = !selected; - }), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 40, - height: 40, - margin: const EdgeInsets.only(right: 15, top: 2.5), - decoration: ShapeDecoration( - shape: const CircleBorder( - side: BorderSide(color: Colors.grey), + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 5), + child: GestureDetector( + onTap: () => setState(() { + selected = !selected; + }), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 40, + height: 40, + margin: const EdgeInsets.only(right: 15, top: 2.5), + decoration: ShapeDecoration( + shape: const CircleBorder( + side: BorderSide(color: Colors.grey), + ), + color: selected + ? Colors.green + : Theme.of(context).colorScheme.background, ), - color: selected - ? Colors.green - : Theme.of(context).colorScheme.background, + child: selected + ? const Icon(Icons.check_outlined) + : Center(child: Text((widget.index + 1).toString())), ), - child: selected - ? const Icon(Icons.check_outlined) - : Center(child: Text((widget.index + 1).toString())), - ), - Expanded( - child: Text( - widget.instruction, + Expanded( + child: Text( + widget.instruction, + ), ), - ), - ], + ], + ), ), - ), - ); - } + ); } diff --git a/lib/src/widget/recipe/widget/nutrition_list.dart b/lib/src/widget/recipe/widget/nutrition_list.dart index cdc7c493..7fee248a 100644 --- a/lib/src/widget/recipe/widget/nutrition_list.dart +++ b/lib/src/widget/recipe/widget/nutrition_list.dart @@ -1,36 +1,34 @@ part of '../recipe_screen.dart'; class NutritionList extends StatelessWidget { - final Map nutrition; - const NutritionList( - this.nutrition, - ); + this.nutrition, { + super.key, + }); + final Map nutrition; @override - Widget build(BuildContext context) { - return ExpansionTile( - title: Text(translate('recipe.fields.nutrition.title')), - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Wrap( - spacing: 10, - runSpacing: 10, - children: [ - for (final entry in nutrition.entries) - RoundedBoxItem( - name: translate( - 'recipe.fields.nutrition.items.${entry.key}', + Widget build(BuildContext context) => ExpansionTile( + title: Text(translate('recipe.fields.nutrition.title')), + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Wrap( + spacing: 10, + runSpacing: 10, + children: [ + for (final entry in nutrition.entries) + RoundedBoxItem( + name: translate( + 'recipe.fields.nutrition.items.${entry.key}', + ), + value: entry.value, + height: 30, + padding: const EdgeInsets.symmetric(horizontal: 10), ), - value: entry.value, - height: 30, - padding: const EdgeInsets.symmetric(horizontal: 10), - ), - ], + ], + ), ), - ), - ], - ); - } + ], + ); } diff --git a/lib/src/widget/recipe/widget/recipe_yield.dart b/lib/src/widget/recipe/widget/recipe_yield.dart index 2bb24db0..760e63d8 100644 --- a/lib/src/widget/recipe/widget/recipe_yield.dart +++ b/lib/src/widget/recipe/widget/recipe_yield.dart @@ -1,8 +1,11 @@ part of '../recipe_screen.dart'; class RecipeYield extends StatelessWidget { + const RecipeYield({ + required this.recipe, + super.key, + }); final Recipe recipe; - const RecipeYield({required this.recipe}); @override Widget build(BuildContext context) { diff --git a/lib/src/widget/recipe/widget/rounded_box_item.dart b/lib/src/widget/recipe/widget/rounded_box_item.dart index 26495f13..d90393bd 100644 --- a/lib/src/widget/recipe/widget/rounded_box_item.dart +++ b/lib/src/widget/recipe/widget/rounded_box_item.dart @@ -1,17 +1,17 @@ part of '../recipe_screen.dart'; class RoundedBoxItem extends StatelessWidget { - final String name; - final String value; - final double? height; - final EdgeInsets? padding; - const RoundedBoxItem({ required this.name, required this.value, this.height, this.padding, + super.key, }); + final String name; + final String value; + final double? height; + final EdgeInsets? padding; @override Widget build(BuildContext context) { diff --git a/lib/src/widget/recipe/widget/tool_list.dart b/lib/src/widget/recipe/widget/tool_list.dart index d889e055..d67d181f 100644 --- a/lib/src/widget/recipe/widget/tool_list.dart +++ b/lib/src/widget/recipe/widget/tool_list.dart @@ -3,26 +3,25 @@ part of '../recipe_screen.dart'; class ToolList extends StatelessWidget { const ToolList({ required this.recipe, + super.key, }); final Recipe recipe; @override - Widget build(BuildContext context) { - return ExpansionTile( - title: Text(translate('recipe.fields.tools')), - children: [ - for (final tool in recipe.tool) - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Text( - "- ${tool.trim()}", + Widget build(BuildContext context) => ExpansionTile( + title: Text(translate('recipe.fields.tools')), + children: [ + for (final tool in recipe.tool) + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + '- ${tool.trim()}', + ), ), ), - ), - ], - ); - } + ], + ); } diff --git a/lib/src/widget/recipe_image.dart b/lib/src/widget/recipe_image.dart index fa16c4f3..86343da0 100644 --- a/lib/src/widget/recipe_image.dart +++ b/lib/src/widget/recipe_image.dart @@ -1,18 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/image_response.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; class RecipeImage extends StatelessWidget { - final Size size; - - final String? id; - const RecipeImage({ - super.key, required this.size, required this.id, + super.key, }); + final Size size; + + final String? id; @override Widget build(BuildContext context) { @@ -22,8 +20,9 @@ class RecipeImage extends StatelessWidget { return SizedBox.fromSize( size: size, child: FutureBuilder( + // ignore: discarded_futures future: id != null ? DataRepository().fetchImage(id!, size) : null, - builder: (context, AsyncSnapshot snapshot) { + builder: (context, snapshot) { if (snapshot.hasData) { if (snapshot.data!.isSvg) { return ColoredBox( diff --git a/lib/src/widget/recipe_list_item.dart b/lib/src/widget/recipe_list_item.dart index a7bb3ef0..258ec34c 100644 --- a/lib/src/widget/recipe_list_item.dart +++ b/lib/src/widget/recipe_list_item.dart @@ -5,49 +5,46 @@ import 'package:nextcloud_cookbook_flutter/src/screens/recipe_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; class RecipeListItem extends StatelessWidget { - final RecipeStub recipe; - const RecipeListItem({ - super.key, required this.recipe, + super.key, }); + final RecipeStub recipe; @override - Widget build(BuildContext context) { - return ListTile( - leading: ClipRRect( - borderRadius: BorderRadius.circular(5), - child: RecipeImage( - id: recipe.recipeId, - size: const Size.square(80), - ), - ), - title: Text(recipe.name), - subtitle: Row( - children: [ - _RecipeListDate( - Icons.edit_calendar_outlined, - recipe.dateCreated, + Widget build(BuildContext context) => ListTile( + leading: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: RecipeImage( + id: recipe.recipeId, + size: const Size.square(80), ), - if (recipe.dateModified != null && - recipe.dateModified != recipe.dateCreated) ...[ + ), + title: Text(recipe.name), + subtitle: Row( + children: [ _RecipeListDate( - Icons.edit_outlined, - recipe.dateModified!, + Icons.edit_calendar_outlined, + recipe.dateCreated, ), + if (recipe.dateModified != null && + recipe.dateModified != recipe.dateCreated) ...[ + _RecipeListDate( + Icons.edit_outlined, + recipe.dateModified!, + ), + ], ], - ], - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RecipeScreen(recipeId: recipe.recipeId), - ), - ); - }, - ); - } + ), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RecipeScreen(recipeId: recipe.recipeId), + ), + ); + }, + ); } class _RecipeListDate extends StatelessWidget { @@ -68,7 +65,7 @@ class _RecipeListDate extends StatelessWidget { return Card( color: colorScheme.secondaryContainer, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), + padding: const EdgeInsets.symmetric(horizontal: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/src/widget/timer_list_item.dart b/lib/src/widget/timer_list_item.dart index 6369e48c..9ff8cec6 100644 --- a/lib/src/widget/timer_list_item.dart +++ b/lib/src/widget/timer_list_item.dart @@ -7,11 +7,12 @@ import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; class TimerListItem extends StatelessWidget { const TimerListItem({ + required this.animation, + required this.item, this.onDismissed, this.dense = false, this.enabled = true, - required this.animation, - required this.item, + super.key, }); final Animation animation; @@ -33,8 +34,8 @@ class TimerListItem extends StatelessWidget { timer: item, ); - void onPressed() { - Navigator.push( + Future onPressed() async { + await Navigator.push( context, MaterialPageRoute( builder: (context) => RecipeScreen(recipeId: item.recipeId), @@ -50,7 +51,7 @@ class TimerListItem extends StatelessWidget { subtitle: dense ? null : progressBar, trailing: IconButton( icon: const Icon(Icons.cancel_outlined), - tooltip: translate("timer.button.cancel"), + tooltip: translate('timer.button.cancel'), onPressed: enabled ? onDismissed : null, ), onTap: (enabled && !dense) ? onPressed : null, diff --git a/lib/src/widget/user_image.dart b/lib/src/widget/user_image.dart index 78f7c32e..b88a7f95 100644 --- a/lib/src/widget/user_image.dart +++ b/lib/src/widget/user_image.dart @@ -16,11 +16,11 @@ class UserImage extends StatelessWidget { return ClipOval( child: CachedNetworkImage( cacheManager: CustomCacheManager().instance, - cacheKey: "avatar", + cacheKey: 'avatar', fit: BoxFit.fill, httpHeaders: { - "Authorization": appAuthentication.basicAuth, - "Accept": "image/jpeg" + 'Authorization': appAuthentication.basicAuth, + 'Accept': 'image/jpeg' }, imageUrl: url, placeholder: (context, url) => ColoredBox( diff --git a/test/models/app_authentication_test.dart b/test/models/app_authentication_test.dart index da8f10d4..389d880e 100644 --- a/test/models/app_authentication_test.dart +++ b/test/models/app_authentication_test.dart @@ -4,15 +4,15 @@ 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( + const server = 'https://example.com'; + const loginName = 'admin'; + const password = 'password'; + final basicAuth = 'Basic ${base64Encode( utf8.encode( '$loginName:$password', ), )}'; - const bool isSelfSignedCertificate = false; + const isSelfSignedCertificate = false; final auth = AppAuthentication( server: server, @@ -29,11 +29,11 @@ void main() { '{"server":"$server","loginName":"$loginName","appPassword":"$password","isSelfSignedCertificate":$isSelfSignedCertificate}'; group(AppAuthentication, () { - test("toJson", () { + test('toJson', () { expect(jsonEncode(auth.toJsonString()), equals(encodedJson)); }); - test("fromJson", () { + test('fromJson', () { expect( AppAuthentication.fromJsonString(jsonBasicAuth), equals(auth), @@ -44,18 +44,18 @@ void main() { ); }); - test("password", () { + test('password', () { expect(auth.password, equals(password)); }); - test("parseBasicAuth", () { + test('parseBasicAuth', () { expect( AppAuthentication.parseBasicAuth(loginName, password), equals(basicAuth), ); }); - test("toJson does not contain password", () { + test('toJson does not contain password', () { expect(auth.toString(), isNot(contains(basicAuth))); }); }); diff --git a/test/models/timer_test.dart b/test/models/timer_test.dart index 51c8b93b..82df4cd3 100644 --- a/test/models/timer_test.dart +++ b/test/models/timer_test.dart @@ -4,11 +4,11 @@ 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 title = 'title'; + const body = 'body'; const duration = Duration(minutes: 5); final done = DateTime.now().add(duration); - const recipeId = "12345678"; + const recipeId = '12345678'; const id = 0; final timer = Timer.restoreOld(done, id, recipeId, title, duration, recipeId); @@ -23,11 +23,11 @@ void main() { final newJson = '{"recipe":null,"done":"${done.toIso8601String()}","id":$id}'; group(Timer, () { - test("toJson", () { + test('toJson', () { expect(jsonEncode(timer.toJson()), equals(newJson)); }); - test("fromJson", () { + test('fromJson', () { expect( Timer.fromJson(jsonDecode(json) as Map), isA(),