diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f659c6c..3bda96bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,7 @@ name: "Build" on: pull_request: + types: [review_requested, ready_for_review] push: jobs: build: @@ -11,7 +12,7 @@ jobs: with: java-version: '12.x' distribution: 'adopt' - - uses: subosito/flutter-action@v2.7.1 + - uses: subosito/flutter-action@v2.8.0 with: flutter-version: '3.3.0' - run: flutter pub get diff --git a/.tx/config b/.tx/config index 6ee36562..cd369f4f 100644 --- a/.tx/config +++ b/.tx/config @@ -1,8 +1,9 @@ [main] host = https://www.transifex.com -[nextcloud.cookbook_flutter] +[o:nextcloud:p:nextcloud:r:cookbook_flutter] file_filter = assets/i18n/.json source_file = assets/i18n/en.json source_lang = en -type = KEYVALUEJSON \ No newline at end of file +type = KEYVALUEJSON + diff --git a/README.md b/README.md index 3f09fff2..12b60444 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ -![GitHub](https://img.shields.io/github/license/Teifun2/nextcloud-cookbook-flutter) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/Teifun2/nextcloud-cookbook-flutter) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/Teifun2/nextcloud-cookbook-flutter/Build/master) +![GitHub](https://img.shields.io/github/license/Teifun2/nextcloud-cookbook-flutter) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/Teifun2/nextcloud-cookbook-flutter) ![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/Teifun2/nextcloud-cookbook-flutter/build.yml?branch=master) [Get it on F-Droid](https://f-droid.org/packages/com.nextcloud_cookbook_flutter/) -[Get it on Google Play](https://play.google.com/store/apps/details?id=com.nextcloud_cookbook_flutter) -[Get it on the App Store](https://apps.apple.com/us/app/nextcloud-cookbook/id1619926634) + height="82" style="padding-bottom: 20px; padding-left: 10px">](https://apps.apple.com/us/app/nextcloud-cookbook/id1619926634) + # Nextcloud Cookbook Mobile Client written in Flutter diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..83448ad4 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,7 @@ +# This file configures the analyzer to use the lint rule set from `package:lint` + +include: package:lint/strict.yaml + +linter: + rules: + sort_pub_dependencies: false diff --git a/assets/i18n/de.json b/assets/i18n/de.json index 82899f86..0e650899 100644 --- a/assets/i18n/de.json +++ b/assets/i18n/de.json @@ -165,7 +165,7 @@ }, "recipe_font_size": { "title": "Rezept Schriftgröße", - "subtitle": "Ändert die Schriftgröße von Rezeptwerkzeugen, Zutaten und Anweisungen für eine bessere Lesbarkeit." + "subtitle": "Ändert die Schriftgröße von \"Utensilien\", \"Zutaten\" und \"Zubereitung\" für eine bessere Lesbarkeit." }, "category_font_size": { "title": "Kategorie Schriftgröße", diff --git a/assets/i18n/en_GB.json b/assets/i18n/en_GB.json new file mode 100644 index 00000000..c3ab051f --- /dev/null +++ b/assets/i18n/en_GB.json @@ -0,0 +1,175 @@ +{ + "app_bar": { + "search": "Search", + "refresh": "Refresh", + "logout": "Logout", + "clear_all": "Clear all" + }, + "login": { + "title": "Login", + "server_url": { + "field": "Server URL", + "validator": { + "empty": "Please enter the URL to your Nextcloud instance.", + "pattern": "Please enter a valid URL" + } + }, + "username": { + "field": "Username" + }, + "password": { + "field": "Password" + }, + "settings": { + "title": "Advanced Settings", + "app_password": "Use self generated App Password.\nNeeded for accounts with 2FA.", + "self_signed_certificate": "Ignore invalid certificate.\nNeeded for self signed certificates.\n(Use at your own risk!)" + }, + "button": "Login", + "retry": "Retry Login!", + "reset": "Reset Credentials!", + "errors": { + "not_reachable": "Cannot reach: {server_url}\n{error_msg}", + "certificate_failed": "The Certificate of the server could not be verified: {server_url}\n{error_msg}", + "request_failed": "App Password request failed:\n{error_msg}", + "parse_failed": "Cannot parse the App Password response!\n{error_msg}", + "parse_missing": "Cannot find App Password in response!\n{error_msg}", + "auth_failed": "Username and / or Password is incorrect!", + "authentication_not_found": "No authentication found in Storage", + "failed_remove_remote": "Failed to remove remote App Password!", + "failure": "Login Process could not finish properly.\n{status_code}\n{status_message}", + "credentials_invalid": "The stored credentials where no longer valid an you have been logged out!", + "no_internet": "Authentication check was not successful.\nMaybe there is no internet connection?\nError Message: {error_msg}", + "wrong_status": "Authentication check was not successful.\nMaybe the provided server host is not correct?\nError Message: {error_msg}" + } + }, + "categories": { + "title": "Cookbook", + "all_categories": "All", + "drawer": { + "import": "Import Recipe", + "settings": "Settings" + }, + "errors": { + "unknown": "Categories in unknown state", + "load_failed": "Category load failed: {error_msg}", + "load_no_response": "Could not retrieve the Categories from the server.", + "api_version_check_failed": "Failed to check the API version of the server:\n{error_msg}", + "api_version_above_confirmed": "The API version of the server was updated. Some features might not work as expected. Please wait for an update!\n{version}", + "plugin_missing": "Categories could not be loaded. Make sure that the Cookbook plugin is installed on this Nextcloud instance!" + } + }, + "recipe_list": { + "title_category": "Category: {category}", + "errors": { + "load_failed": "Failed to load Recipes Short!" + } + }, + "recipe": { + "title": "Recipe:", + "prep": "Preparation time", + "cook": "Cooking time", + "total": "Total time", + "fields": { + "name": "Recipe Name:", + "description": "Recipe Description:", + "keywords": "Keywords:", + "category": "Category:", + "servings": "Servings:", + "source": "Source:", + "source_button": "Source", + "image": "Image:", + "time": { + "prep": "Preparation time:", + "cook": "Cooking time:", + "total": "Total time:", + "hours": "Hours", + "minutes": "Minutes" + }, + "nutrition": { + "title": "Nutrition Information", + "items": { + "calories": "Energy", + "carbohydrateContent": "Carbohydrate", + "cholesterolContent": "Cholesterol", + "fatContent": "Fat total", + "fiberContent": "Fiber", + "proteinContent": "Protein", + "saturatedFatContent": "Saturated Fat", + "servingSize": "Serving size", + "sodiumContent": "Sodium", + "sugarContent": "Sugar", + "transFatContent": "Trans-fat content", + "unsaturatedFatContent": "Unsaturated-fat content" + } + }, + "tools": "Tools:", + "ingredients": "Ingredients:", + "instructions": "Instructions:" + }, + "errors": { + "load_failed": "Failed to load Recipe!" + } + }, + "recipe_edit": { + "title": "Edit Recipe", + "button": "Save", + "errors": { + "update_failed": "Update Failed {error_msg}" + } + }, + "recipe_create": { + "title": "Create Recipe", + "button": "Create", + "errors": { + "update_failed": "Create Failed {error_msg}" + } + }, + "recipe_import": { + "title": "Import Recipe", + "button": "Import", + "field": "URL to Recipe", + "clipboard": "Paste Clipboard", + "errors": { + "import_failed": "Import Failed {error_msg}" + } + }, + "search": { + "title": "Search Recipe", + "nothing_found": "No recipe found!", + "errors": { + "search_failed": "Unable to load all Recipes!\n{error_msg}" + } + }, + "timer": { + "title": "Your timers", + "started": "Timer started.", + "finished": "is finished.", + "done": "Timer is done.", + "missing": "You need to set the cooking time to use a timer." + }, + "settings": { + "title": "App Settings", + "dark_mode": { + "title": "Dark Mode", + "system": "System Default", + "dark": "Always", + "light": "Never" + }, + "language": { + "title": "Language" + }, + "stay_awake": { + "title": "Stay Awake", + "subtitle": "Screen will stay on while on the recipe screen." + }, + "recipe_font_size": { + "title": "Recipe Font Size", + "subtitle": "Changes the font size of recipe tools, ingredients and instructions for better readability." + }, + "category_font_size": { + "title": "Category Font Size", + "subtitle": "Changes the font size of category title. This can be useful if you have long category names." + } + } +} diff --git a/assets/i18n/fr.json b/assets/i18n/fr.json index 375feaba..5528ba33 100644 --- a/assets/i18n/fr.json +++ b/assets/i18n/fr.json @@ -62,7 +62,7 @@ "recipe_list": { "title_category": "Catégorie : {category}", "errors": { - "load_failed": "Failed to load Recipes Short!" + "load_failed": "Impossible de charger le raccourci de recette!" } }, "recipe": { diff --git a/assets/i18n/nl.json b/assets/i18n/nl.json index bc140f12..7a2ea26a 100644 --- a/assets/i18n/nl.json +++ b/assets/i18n/nl.json @@ -27,7 +27,7 @@ }, "button": "Inloggen", "retry": "Retry Login!", - "reset": "Reset Credentials!", + "reset": "Reset inloggegevens", "errors": { "not_reachable": "Kan {server_url} niet bereiken\n {error_msg}", "certificate_failed": "Het certificaat van de server kon niet worden geverifieerd: {server_url} \n {error_msg}", @@ -39,8 +39,8 @@ "failed_remove_remote": "Kon het remote App-wachtwoord niet verwijderen!", "failure": "Het inlogproces kon niet goed eindigen.\n {status_code}\n {status_message}", "credentials_invalid": "De opgeslagen inloggegevens zijn niet meer geldig en je bent uitgelogd!", - "no_internet": "Authentication check was not successful.\nMaybe there is no internet connection?\nError Message: {error_msg}", - "wrong_status": "Authentication check was not successful.\nMaybe the provided server host is not correct?\nError Message: {error_msg}" + "no_internet": "Authenticatie is niet gelukt.\nMisschien is er geen internetverbinding?\nFoutmelding: {error_msg}", + "wrong_status": "Authenticatiecontrole is niet gelukt.\nMisschien is de opgegeven host niet correct?\nFoutmelding: {error_msg}" } }, "categories": { diff --git a/assets/i18n/zh_CN.json b/assets/i18n/zh_CN.json index b42751e4..c61181b6 100644 --- a/assets/i18n/zh_CN.json +++ b/assets/i18n/zh_CN.json @@ -26,7 +26,7 @@ "self_signed_certificate": "忽略无效证书对自签名证书是必须的(使用风险自担!)" }, "button": "登录", - "retry": "Retry Login!", + "retry": "重试登录!", "reset": "Reset Credentials!", "errors": { "not_reachable": "无法抵达:{server_url}{error_msg}", diff --git a/build.yaml b/build.yaml new file mode 100644 index 00000000..61f6ccfc --- /dev/null +++ b/build.yaml @@ -0,0 +1,5 @@ +targets: + $default: + builders: + copy_with_extension_gen: + enabled: true diff --git a/fastlane/metadata/android/en-US/changelogs/24.txt b/fastlane/metadata/android/en-US/changelogs/24.txt new file mode 100644 index 00000000..4d6ce17b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/24.txt @@ -0,0 +1,2 @@ +- API Update +- Bugfixes \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 70926c36..fe903ba1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,16 +1,18 @@ -import 'package:bloc/bloc.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/categories/categories.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/simple_bloc_delegatae.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/category/category_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/loading_screen.dart'; +import 'package:nextcloud_cookbook_flutter/src/screens/login_screen.dart'; +import 'package:nextcloud_cookbook_flutter/src/screens/splash_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/services/intent_repository.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/notification_provider.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; import 'package:nextcloud_cookbook_flutter/src/util/lifecycle_event_handler.dart'; import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; import 'package:nextcloud_cookbook_flutter/src/util/supported_locales.dart'; @@ -18,16 +20,9 @@ import 'package:nextcloud_cookbook_flutter/src/util/theme_mode_manager.dart'; import 'package:nextcloud_cookbook_flutter/src/util/translate_preferences.dart'; import 'package:theme_mode_handler/theme_mode_handler.dart'; -import './src/screens/login_screen.dart'; -import './src/screens/splash_screen.dart'; -import './src/services/notification_provider.dart'; -import './src/services/user_repository.dart'; -import 'src/blocs/authentication/authentication.dart'; -import 'src/blocs/simple_bloc_delegatae.dart'; - void main() async { Bloc.observer = SimpleBlocDelegate(); - var delegate = await LocalizationDelegate.create( + final delegate = await LocalizationDelegate.create( basePath: 'assets/i18n/', fallbackLocale: 'en', supportedLocales: SupportedLocales.locales.keys.toList(), @@ -58,13 +53,15 @@ void main() async { }, ) ], - child: App(), + child: const App(), ), ), ); } class App extends StatefulWidget { + const App({super.key}); + @override State createState() => _AppState(); } @@ -72,6 +69,7 @@ class App extends StatefulWidget { class _AppState extends State { final UserRepository userRepository = UserRepository(); + @override void initState() { super.initState(); @@ -84,13 +82,10 @@ class _AppState extends State { ); // Update Localization if Settings are set! - String savedLocalization = Settings.getValue( - describeEnum(SettingKeys.language), - 'default', + final savedLocalization = Settings.getValue( + SettingKeys.language.name, ); - if (savedLocalization != 'default') { - changeLocale(context, savedLocalization); - } + changeLocale(context, savedLocalization); } @override @@ -112,7 +107,7 @@ class _AppState extends State { home: BlocBuilder( builder: (context, state) { if (state is AuthenticationUninitialized) { - return SplashPage(); + return const SplashPage(); } else if (state is AuthenticationAuthenticated) { IntentRepository().handleIntent(); if (BlocProvider.of(context).state @@ -120,18 +115,18 @@ class _AppState extends State { BlocProvider.of(context) .add(CategoriesLoaded()); } - return CategoryScreen(); + return const CategoryScreen(); } else if (state is AuthenticationUnauthenticated) { - return LoginScreen(); + return const LoginScreen(); } else if (state is AuthenticationInvalid) { - return LoginScreen( + return const LoginScreen( invalidCredentials: true, ); } else if (state is AuthenticationLoading || state is AuthenticationError) { - return LoadingScreen(); + return const LoadingScreen(); } else { - return LoadingScreen(); + return const LoadingScreen(); } }, ), diff --git a/lib/src/blocs/authentication/authentication.dart b/lib/src/blocs/authentication/authentication.dart index d21b0e46..69b98588 100644 --- a/lib/src/blocs/authentication/authentication.dart +++ b/lib/src/blocs/authentication/authentication.dart @@ -1,3 +1,3 @@ export 'authentication_bloc.dart'; export 'authentication_event.dart'; -export 'authentication_state.dart'; \ No newline at end of file +export 'authentication_state.dart'; diff --git a/lib/src/blocs/authentication/authentication_bloc.dart b/lib/src/blocs/authentication/authentication_bloc.dart index 3ae893fa..4a4acad2 100644 --- a/lib/src/blocs/authentication/authentication_bloc.dart +++ b/lib/src/blocs/authentication/authentication_bloc.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../services/user_repository.dart'; -import 'authentication.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; class AuthenticationBloc extends Bloc { @@ -13,7 +13,8 @@ class AuthenticationBloc @override Stream mapEventToState( - AuthenticationEvent event) async* { + AuthenticationEvent event, + ) async* { if (event is AppStarted) { final bool hasToken = await userRepository.hasAppAuthentication(); @@ -24,7 +25,7 @@ class AuthenticationBloc try { validCredentials = await userRepository.checkAppAuthentication(); } catch (e) { - yield AuthenticationError(e); + yield AuthenticationError(e.toString()); return; } if (validCredentials) { diff --git a/lib/src/blocs/authentication/authentication_event.dart b/lib/src/blocs/authentication/authentication_event.dart index 9688a6bc..81177a0f 100644 --- a/lib/src/blocs/authentication/authentication_event.dart +++ b/lib/src/blocs/authentication/authentication_event.dart @@ -1,4 +1,3 @@ -import 'package:meta/meta.dart'; import 'package:equatable/equatable.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; @@ -14,7 +13,7 @@ class AppStarted extends AuthenticationEvent {} class LoggedIn extends AuthenticationEvent { final AppAuthentication appAuthentication; - const LoggedIn({@required this.appAuthentication}); + const LoggedIn({required this.appAuthentication}); @override List get props => [appAuthentication]; diff --git a/lib/src/blocs/categories/categories_bloc.dart b/lib/src/blocs/categories/categories_bloc.dart index 293c5e89..4b83884d 100644 --- a/lib/src/blocs/categories/categories_bloc.dart +++ b/lib/src/blocs/categories/categories_bloc.dart @@ -1,4 +1,5 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:nextcloud_cookbook_flutter/src/blocs/categories/categories.dart'; import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; @@ -18,10 +19,10 @@ class CategoriesBloc extends Bloc { Stream _mapCategoriesLoadedToState() async* { try { yield CategoriesLoadInProgress(); - List categories = await dataRepository.fetchCategories(); + final List categories = await dataRepository.fetchCategories(); dataRepository.updateCategoryNames(categories); yield CategoriesLoadSuccess(categories: categories); - List categoriesWithImage = + final List categoriesWithImage = await dataRepository.fetchCategoryMainRecipes(categories); yield CategoriesImageLoadSuccess(categories: categoriesWithImage); } on Exception catch (e) { diff --git a/lib/src/blocs/categories/categories_state.dart b/lib/src/blocs/categories/categories_state.dart index c90001c5..7fbcbabc 100644 --- a/lib/src/blocs/categories/categories_state.dart +++ b/lib/src/blocs/categories/categories_state.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; abstract class CategoriesState extends Equatable { @@ -14,7 +13,7 @@ class CategoriesInitial extends CategoriesState {} class CategoriesLoadSuccess extends CategoriesState { final List categories; - const CategoriesLoadSuccess({@required this.categories}); + const CategoriesLoadSuccess({required this.categories}); @override List get props => [categories]; @@ -23,7 +22,7 @@ class CategoriesLoadSuccess extends CategoriesState { class CategoriesImageLoadSuccess extends CategoriesState { final List categories; - const CategoriesImageLoadSuccess({@required this.categories}); + const CategoriesImageLoadSuccess({required this.categories}); @override List get props => [categories]; diff --git a/lib/src/blocs/login/login.dart b/lib/src/blocs/login/login.dart index 19f0fa2a..7aff76e4 100644 --- a/lib/src/blocs/login/login.dart +++ b/lib/src/blocs/login/login.dart @@ -1,3 +1,3 @@ +export 'login_bloc.dart'; export 'login_event.dart'; export 'login_state.dart'; -export 'login_bloc.dart'; \ No newline at end of file diff --git a/lib/src/blocs/login/login_bloc.dart b/lib/src/blocs/login/login_bloc.dart index 3ab89e35..743fdff5 100644 --- a/lib/src/blocs/login/login_bloc.dart +++ b/lib/src/blocs/login/login_bloc.dart @@ -1,22 +1,19 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/login/login.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; - -import '../../services/user_repository.dart'; -import 'login.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; class LoginBloc extends Bloc { final UserRepository userRepository = UserRepository(); final AuthenticationBloc authenticationBloc; LoginBloc({ - @required this.authenticationBloc, - }) : super(LoginInitial()) { - assert(authenticationBloc != null); - } + required this.authenticationBloc, + }) : super(LoginInitial()); @override Stream mapEventToState(LoginEvent event) async* { @@ -31,14 +28,14 @@ class LoginBloc extends Bloc { event.serverURL, event.username, event.originalBasicAuth, - event.isSelfSignedCertificate, + isSelfSignedCertificate: event.isSelfSignedCertificate, ); } else { appAuthentication = await userRepository.authenticate( event.serverURL, event.username, event.originalBasicAuth, - event.isSelfSignedCertificate, + isSelfSignedCertificate: event.isSelfSignedCertificate, ); } diff --git a/lib/src/blocs/login/login_event.dart b/lib/src/blocs/login/login_event.dart index 217a4249..06ec8f96 100644 --- a/lib/src/blocs/login/login_event.dart +++ b/lib/src/blocs/login/login_event.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:meta/meta.dart'; abstract class LoginEvent extends Equatable { const LoginEvent(); @@ -16,11 +15,11 @@ class LoginButtonPressed extends LoginEvent { final bool isSelfSignedCertificate; const LoginButtonPressed({ - @required this.serverURL, - @required this.username, - @required this.originalBasicAuth, - @required this.isAppPassword, - @required this.isSelfSignedCertificate, + required this.serverURL, + required this.username, + required this.originalBasicAuth, + required this.isAppPassword, + required this.isSelfSignedCertificate, }); @override diff --git a/lib/src/blocs/login/login_state.dart b/lib/src/blocs/login/login_state.dart index a9ef395d..d7800b4c 100644 --- a/lib/src/blocs/login/login_state.dart +++ b/lib/src/blocs/login/login_state.dart @@ -1,4 +1,3 @@ -import 'package:meta/meta.dart'; import 'package:equatable/equatable.dart'; abstract class LoginState extends Equatable { @@ -15,7 +14,7 @@ class LoginLoading extends LoginState {} class LoginFailure extends LoginState { final String error; - const LoginFailure({@required this.error}); + const LoginFailure({required this.error}); @override List get props => [error]; diff --git a/lib/src/blocs/recipe/recipe.dart b/lib/src/blocs/recipe/recipe.dart index 569e24ba..d2dc0754 100644 --- a/lib/src/blocs/recipe/recipe.dart +++ b/lib/src/blocs/recipe/recipe.dart @@ -1,3 +1,3 @@ export 'recipe_bloc.dart'; export 'recipe_event.dart'; -export 'recipe_state.dart'; \ No newline at end of file +export 'recipe_state.dart'; diff --git a/lib/src/blocs/recipe/recipe_bloc.dart b/lib/src/blocs/recipe/recipe_bloc.dart index 5540b77f..5ed4414e 100644 --- a/lib/src/blocs/recipe/recipe_bloc.dart +++ b/lib/src/blocs/recipe/recipe_bloc.dart @@ -1,4 +1,4 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; @@ -22,7 +22,8 @@ class RecipeBloc extends Bloc { } Stream _mapRecipeLoadedToState( - RecipeLoaded recipeLoaded) async* { + RecipeLoaded recipeLoaded, + ) async* { try { yield RecipeLoadInProgress(); final recipe = await dataRepository.fetchRecipe(recipeLoaded.recipeId); @@ -33,10 +34,12 @@ class RecipeBloc extends Bloc { } Stream _mapRecipeUpdatedToState( - RecipeUpdated recipeUpdated) async* { + RecipeUpdated recipeUpdated, + ) async* { try { yield RecipeUpdateInProgress(); - int recipeId = await dataRepository.updateRecipe(recipeUpdated.recipe); + final String recipeId = + await dataRepository.updateRecipe(recipeUpdated.recipe); yield RecipeUpdateSuccess(recipeId); } catch (_) { yield RecipeUpdateFailure(_.toString()); @@ -44,10 +47,12 @@ class RecipeBloc extends Bloc { } Stream _mapRecipeCreatedToState( - RecipeCreated recipeCreated) async* { + RecipeCreated recipeCreated, + ) async* { try { yield RecipeCreateInProgress(); - int recipeId = await dataRepository.createRecipe(recipeCreated.recipe); + final String recipeId = + await dataRepository.createRecipe(recipeCreated.recipe); yield RecipeCreateSuccess(recipeId); } catch (_) { yield RecipeCreateFailure(_.toString()); @@ -55,10 +60,12 @@ class RecipeBloc extends Bloc { } Stream _mapRecipeImportedToState( - RecipeImported recipeImported) async* { + RecipeImported recipeImported, + ) async* { try { yield RecipeImportInProgress(); - Recipe recipe = await dataRepository.importRecipe(recipeImported.url); + final Recipe recipe = + await dataRepository.importRecipe(recipeImported.url); yield RecipeImportSuccess(recipe.id); } catch (_) { yield RecipeImportFailure(_.toString()); diff --git a/lib/src/blocs/recipe/recipe_event.dart b/lib/src/blocs/recipe/recipe_event.dart index 80802838..050e36e4 100644 --- a/lib/src/blocs/recipe/recipe_event.dart +++ b/lib/src/blocs/recipe/recipe_event.dart @@ -1,21 +1,20 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; abstract class RecipeEvent extends Equatable { const RecipeEvent(); @override - List get props => []; + List get props => []; } class RecipeLoaded extends RecipeEvent { - final int recipeId; + final String recipeId; - const RecipeLoaded({@required this.recipeId}); + const RecipeLoaded(this.recipeId); @override - List get props => [recipeId]; + List get props => [recipeId]; } class RecipeUpdated extends RecipeEvent { @@ -24,7 +23,7 @@ class RecipeUpdated extends RecipeEvent { const RecipeUpdated(this.recipe); @override - List get props => [recipe]; + List get props => [recipe]; } class RecipeCreated extends RecipeEvent { @@ -33,7 +32,7 @@ class RecipeCreated extends RecipeEvent { const RecipeCreated(this.recipe); @override - List get props => [recipe]; + List get props => [recipe]; } class RecipeImported extends RecipeEvent { @@ -42,5 +41,5 @@ class RecipeImported extends RecipeEvent { const RecipeImported(this.url); @override - List get props => [url]; + List get props => [url]; } diff --git a/lib/src/blocs/recipe/recipe_state.dart b/lib/src/blocs/recipe/recipe_state.dart index aa388d11..ad987d4b 100644 --- a/lib/src/blocs/recipe/recipe_state.dart +++ b/lib/src/blocs/recipe/recipe_state.dart @@ -5,7 +5,7 @@ abstract class RecipeState extends Equatable { const RecipeState(); @override - List get props => []; + List get props => []; } class RecipeInitial extends RecipeState {} @@ -29,36 +29,36 @@ class RecipeSuccess extends RecipeState { } class RecipeLoadSuccess extends RecipeSuccess { - RecipeLoadSuccess(Recipe recipe) : super(recipe); + const RecipeLoadSuccess(super.recipe); } class RecipeLoadFailure extends RecipeFailure { - RecipeLoadFailure(String errorMsg) : super(errorMsg); + const RecipeLoadFailure(super.errorMsg); } class RecipeLoadInProgress extends RecipeState {} class RecipeUpdateFailure extends RecipeFailure { - RecipeUpdateFailure(String errorMsg) : super(errorMsg); + const RecipeUpdateFailure(super.errorMsg); } class RecipeUpdateSuccess extends RecipeState { - final int recipeId; + final String recipeId; const RecipeUpdateSuccess(this.recipeId); @override - List get props => [recipeId]; + List get props => [recipeId]; } class RecipeUpdateInProgress extends RecipeState {} class RecipeCreateFailure extends RecipeFailure { - RecipeCreateFailure(String errorMsg) : super(errorMsg); + const RecipeCreateFailure(super.errorMsg); } class RecipeCreateSuccess extends RecipeState { - final int recipeId; + final String recipeId; const RecipeCreateSuccess(this.recipeId); @@ -69,16 +69,16 @@ class RecipeCreateSuccess extends RecipeState { class RecipeCreateInProgress extends RecipeState {} class RecipeImportSuccess extends RecipeState { - final int recipeId; + final String recipeId; const RecipeImportSuccess(this.recipeId); @override - List get props => [recipeId]; + List get props => [recipeId]; } class RecipeImportFailure extends RecipeFailure { - RecipeImportFailure(String errorMsg) : super(errorMsg); + const RecipeImportFailure(super.errorMsg); } class RecipeImportInProgress extends RecipeState {} diff --git a/lib/src/blocs/recipes_short/recipes_short.dart b/lib/src/blocs/recipes_short/recipes_short.dart index 0496d4a1..496cae75 100644 --- a/lib/src/blocs/recipes_short/recipes_short.dart +++ b/lib/src/blocs/recipes_short/recipes_short.dart @@ -1,3 +1,3 @@ export 'recipes_short_bloc.dart'; export 'recipes_short_event.dart'; -export 'recipes_short_state.dart'; \ No newline at end of file +export 'recipes_short_state.dart'; diff --git a/lib/src/blocs/recipes_short/recipes_short_bloc.dart b/lib/src/blocs/recipes_short/recipes_short_bloc.dart index c15ca406..c32a4785 100644 --- a/lib/src/blocs/recipes_short/recipes_short_bloc.dart +++ b/lib/src/blocs/recipes_short/recipes_short_bloc.dart @@ -1,4 +1,4 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_event.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_state.dart'; import 'package:nextcloud_cookbook_flutter/src/services/data_repository.dart'; @@ -18,10 +18,12 @@ class RecipesShortBloc extends Bloc { } Stream _mapRecipesShortLoadedToState( - RecipesShortLoaded recipesShortLoaded) async* { + RecipesShortLoaded recipesShortLoaded, + ) async* { try { final recipesShort = await dataRepository.fetchRecipesShort( - category: recipesShortLoaded.category); + category: recipesShortLoaded.category, + ); yield RecipesShortLoadSuccess(recipesShort); } catch (_) { yield RecipesShortLoadFailure(); @@ -29,7 +31,8 @@ class RecipesShortBloc extends Bloc { } Stream _mapRecipesShortLoadedAllToState( - RecipesShortLoadedAll recipesShortLoadedAll) async* { + RecipesShortLoadedAll recipesShortLoadedAll, + ) async* { try { yield RecipesShortLoadAllInProgress(); final recipesShort = await dataRepository.fetchAllRecipes(); diff --git a/lib/src/blocs/recipes_short/recipes_short_event.dart b/lib/src/blocs/recipes_short/recipes_short_event.dart index 0f93b2be..cbf4a8e7 100644 --- a/lib/src/blocs/recipes_short/recipes_short_event.dart +++ b/lib/src/blocs/recipes_short/recipes_short_event.dart @@ -10,10 +10,10 @@ abstract class RecipesShortEvent extends Equatable { class RecipesShortLoaded extends RecipesShortEvent { final String category; - const RecipesShortLoaded({this.category}); + const RecipesShortLoaded({required this.category}); @override - List get props => [category]; + List get props => [category]; } class RecipesShortLoadedAll extends RecipesShortEvent {} diff --git a/lib/src/blocs/recipes_short/recipes_short_state.dart b/lib/src/blocs/recipes_short/recipes_short_state.dart index 3a347336..60889b00 100644 --- a/lib/src/blocs/recipes_short/recipes_short_state.dart +++ b/lib/src/blocs/recipes_short/recipes_short_state.dart @@ -15,10 +15,10 @@ class RecipesShortLoadFailure extends RecipesShortState {} class RecipesShortLoadSuccess extends RecipesShortState { final List recipesShort; - const RecipesShortLoadSuccess([this.recipesShort = const []]); + const RecipesShortLoadSuccess(this.recipesShort); @override - List get props => [recipesShort]; + List get props => recipesShort; @override String toString() => 'RecipesShortLoadSuccess { recipes: $recipesShort }'; @@ -27,10 +27,10 @@ class RecipesShortLoadSuccess extends RecipesShortState { class RecipesShortLoadAllSuccess extends RecipesShortState { final List recipesShort; - const RecipesShortLoadAllSuccess([this.recipesShort = const []]); + const RecipesShortLoadAllSuccess(this.recipesShort); @override - List get props => [recipesShort]; + List get props => recipesShort; @override String toString() => 'RecipesShortLoadAllSuccess { recipes: $recipesShort }'; @@ -42,7 +42,7 @@ class RecipesShortLoadAllFailure extends RecipesShortState { const RecipesShortLoadAllFailure(this.errorMsg); @override - List get props => [errorMsg]; + List get props => [errorMsg]; } class RecipesShortLoadAllInProgress extends RecipesShortState {} diff --git a/lib/src/blocs/simple_bloc_delegatae.dart b/lib/src/blocs/simple_bloc_delegatae.dart index 2a67beb6..e43d0897 100644 --- a/lib/src/blocs/simple_bloc_delegatae.dart +++ b/lib/src/blocs/simple_bloc_delegatae.dart @@ -1,21 +1,22 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SimpleBlocDelegate extends BlocObserver { @override - void onEvent(Bloc bloc, Object event) { - print(event); + void onEvent(Bloc bloc, Object? event) { + debugPrint(event?.toString()); super.onEvent(bloc, event); } @override void onTransition(Bloc bloc, Transition transition) { - print(transition); + debugPrint(transition.toString()); super.onTransition(bloc, transition); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { - print(error); + debugPrint(error.toString()); super.onError(bloc, error, stackTrace); } } diff --git a/lib/src/models/app_authentication.dart b/lib/src/models/app_authentication.dart index 96212567..617809c8 100644 --- a/lib/src/models/app_authentication.dart +++ b/lib/src/models/app_authentication.dart @@ -5,50 +5,48 @@ import 'package:dio/dio.dart'; import 'package:nextcloud_cookbook_flutter/src/util/self_signed_certificate_http_overrides.dart'; class AppAuthentication { - String server; - String loginName; - String basicAuth; - bool isSelfSignedCertificate; + final String server; + final String loginName; + final String basicAuth; + final bool isSelfSignedCertificate; - Dio authenticatedClient; + final Dio authenticatedClient = Dio(); AppAuthentication({ - this.server, - this.loginName, - this.basicAuth, - this.isSelfSignedCertificate, + required this.server, + required this.loginName, + required this.basicAuth, + required this.isSelfSignedCertificate, }) { - authenticatedClient = Dio(); authenticatedClient.options.headers["authorization"] = basicAuth; authenticatedClient.options.headers["User-Agent"] = "Cookbook App"; authenticatedClient.options.responseType = ResponseType.plain; if (isSelfSignedCertificate) { - HttpOverrides.global = new SelfSignedCertificateHttpOverride(); + HttpOverrides.global = SelfSignedCertificateHttpOverride(); } } factory AppAuthentication.fromJson(String jsonString) { - Map jsonData = json.decode(jsonString); + final jsonData = json.decode(jsonString) as Map; - String basicAuth = jsonData.containsKey("basicAuth") - ? jsonData['basicAuth'] - : 'Basic ' + - base64Encode( - utf8.encode( - '${jsonData["loginName"]}:${jsonData["appPassword"]}', - ), - ); + final basicAuth = jsonData.containsKey("basicAuth") + ? jsonData['basicAuth'] as String + : 'Basic ${base64Encode( + utf8.encode( + '${jsonData["loginName"]}:${jsonData["appPassword"]}', + ), + )}'; - bool selfSignedCertificate = jsonData.containsKey("isSelfSignedCertificate") - ? jsonData['isSelfSignedCertificate'] - : false; + final selfSignedCertificate = + jsonData['isSelfSignedCertificate'] as bool? ?? false; return AppAuthentication( - server: jsonData["server"], - loginName: jsonData["loginName"], - basicAuth: basicAuth, - isSelfSignedCertificate: selfSignedCertificate); + server: jsonData["server"] as String, + loginName: jsonData["loginName"] as String, + basicAuth: basicAuth, + isSelfSignedCertificate: selfSignedCertificate, + ); } String toJson() { diff --git a/lib/src/models/category.dart b/lib/src/models/category.dart index 4f8b0460..e050793e 100644 --- a/lib/src/models/category.dart +++ b/lib/src/models/category.dart @@ -1,28 +1,43 @@ import 'dart:convert'; +import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:equatable/equatable.dart'; +part 'category.g.dart'; + +@CopyWith(constructor: "_") class Category extends Equatable { + @CopyWithField(immutable: true) final String name; + @CopyWithField(immutable: true) final int recipeCount; - int firstRecipeId; + final String? firstRecipeId; + + const Category(this.name, this.recipeCount) : firstRecipeId = null; - Category(this.name, this.recipeCount); + const Category._({ + required this.name, + required this.recipeCount, + required this.firstRecipeId, + }); Category.fromJson(Map json) - : name = json["name"], + : name = json["name"] as String, recipeCount = json["recipe_count"] is int - ? json["recipe_count"] - : int.parse(json["recipe_count"]); + ? json["recipe_count"] as int + : int.parse(json["recipe_count"] as String), + firstRecipeId = null; @override - List get props => [name]; + List get props => [name]; static List parseCategories(String responseBody) { - final parsed = json.decode(responseBody).cast>(); + final parsed = json.decode(responseBody) as List; return parsed - .map((json) => Category.fromJson(json)) + .map( + (json) => Category.fromJson(json as Map), + ) .where((Category c) => c.recipeCount > 0) .toList(); } diff --git a/lib/src/models/category.g.dart b/lib/src/models/category.g.dart new file mode 100644 index 00000000..72cd498c --- /dev/null +++ b/lib/src/models/category.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'category.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$CategoryCWProxy { + Category firstRecipeId(String? firstRecipeId); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Category(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// Category(...).copyWith(id: 12, name: "My name") + /// ```` + Category call({ + String? firstRecipeId, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfCategory.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfCategory.copyWith.fieldName(...)` +class _$CategoryCWProxyImpl implements _$CategoryCWProxy { + const _$CategoryCWProxyImpl(this._value); + + final Category _value; + + @override + Category firstRecipeId(String? firstRecipeId) => + this(firstRecipeId: firstRecipeId); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Category(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// Category(...).copyWith(id: 12, name: "My name") + /// ```` + Category call({ + Object? firstRecipeId = const $CopyWithPlaceholder(), + }) { + return Category._( + name: _value.name, + recipeCount: _value.recipeCount, + firstRecipeId: firstRecipeId == const $CopyWithPlaceholder() + ? _value.firstRecipeId + // ignore: cast_nullable_to_non_nullable + : firstRecipeId as String?, + ); + } +} + +extension $CategoryCopyWith on Category { + /// Returns a callable class that can be used as follows: `instanceOfCategory.copyWith(...)` or like so:`instanceOfCategory.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$CategoryCWProxy get copyWith => _$CategoryCWProxyImpl(this); +} diff --git a/lib/src/models/intial_login.dart b/lib/src/models/intial_login.dart index 409e1959..e8dda40f 100644 --- a/lib/src/models/intial_login.dart +++ b/lib/src/models/intial_login.dart @@ -1,21 +1,21 @@ -import 'poll.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/poll.dart'; class InitialLogin { Poll poll; String login; InitialLogin({ - this.poll, - this.login, + required this.poll, + required this.login, }); factory InitialLogin.fromJson(Map json) => InitialLogin( - poll: Poll.fromJson(json["poll"]), - login: json["login"], - ); + poll: Poll.fromJson(json["poll"] as Map), + login: json["login"] as String, + ); Map toJson() => { - "poll": poll.toJson(), - "login": login, - }; -} \ No newline at end of file + "poll": poll.toJson(), + "login": login, + }; +} diff --git a/lib/src/models/poll.dart b/lib/src/models/poll.dart index ee9d582d..43a78084 100644 --- a/lib/src/models/poll.dart +++ b/lib/src/models/poll.dart @@ -3,17 +3,17 @@ class Poll { String endpoint; Poll({ - this.token, - this.endpoint, + required this.token, + required this.endpoint, }); factory Poll.fromJson(Map json) => Poll( - token: json["token"], - endpoint: json["endpoint"], - ); + token: json["token"] as String, + endpoint: json["endpoint"] as String, + ); Map toJson() => { - "token": token, - "endpoint": endpoint, - }; -} \ No newline at end of file + "token": token, + "endpoint": endpoint, + }; +} diff --git a/lib/src/models/recipe.dart b/lib/src/models/recipe.dart index 301b0bc8..887f523b 100644 --- a/lib/src/models/recipe.dart +++ b/lib/src/models/recipe.dart @@ -5,7 +5,7 @@ import 'package:nextcloud_cookbook_flutter/src/util/iso_time_format.dart'; import 'package:nextcloud_cookbook_flutter/src/util/nutrition_utilty.dart'; class Recipe extends Equatable { - final int id; + final String id; final String name; final String imageUrl; final String recipeCategory; @@ -23,111 +23,73 @@ class Recipe extends Equatable { final String url; final Map remainingData; - const Recipe._( - this.id, - this.name, - this.imageUrl, - this.recipeCategory, - this.description, - this.nutrition, - this.recipeIngredient, - this.recipeInstructions, - this.tool, - this.recipeYield, - this.prepTime, - this.cookTime, - this.totalTime, - this.keywords, - this.image, - this.url, - this.remainingData); - - factory Recipe.empty() { - return Recipe._( - 0, - '', - '', - '', - '', - Map(), - List.empty(), - List.empty(), - List.empty(), - 1, - Duration.zero, - Duration.zero, - Duration.zero, - '', - '', - '', - Map(), - ); - } - factory Recipe(String jsonString) { - Map data = json.decode(jsonString); + final data = json.decode(jsonString) as Map; - int id = data["id"]; - String name = data["name"]; - String imageUrl = data["imageUrl"]; - String recipeCategory = data["recipeCategory"]; - String description = data["description"]; + final String id = + data["id"] is int ? data["id"]!.toString() : data["id"] as String; + final String name = data["name"] as String; + final String imageUrl = data["imageUrl"] as String; + final String recipeCategory = data["recipeCategory"] as String; + final String description = data["description"] as String; Map recipeNutrition = {}; if (data["nutrition"] is Map) { recipeNutrition = (data["nutrition"] as Map) - .map((key, value) => MapEntry(key, value?.toString())) - ..removeWhere((key, value) => - !NutritionUtility.nutritionProperties.contains(key)); + .map((key, value) => MapEntry(key, value.toString())) + ..removeWhere( + (key, value) => !NutritionUtility.nutritionProperties.contains(key), + ); data["nutrition"] = (data["nutrition"] as Map) .map((key, value) => MapEntry(key, value?.toString())) ..removeWhere( - (key, value) => NutritionUtility.nutritionProperties.contains(key)); + (key, value) => NutritionUtility.nutritionProperties.contains(key), + ); } List recipeIngredient = []; if (data["recipeIngredient"] is Map) { - data["recipeIngredient"] + (data["recipeIngredient"] as Map) .forEach((k, v) => recipeIngredient.add(v as String)); } else if (data["recipeIngredient"] != null) { - recipeIngredient = data["recipeIngredient"].cast().toList(); + recipeIngredient = (data["recipeIngredient"] as List).cast(); } List recipeInstructions = []; if (data["recipeInstructions"] is Map) { - data["recipeInstructions"] + (data["recipeInstructions"] as Map) .forEach((k, v) => recipeInstructions.add(v as String)); } else if (data["recipeInstructions"] != null) { - recipeInstructions = data["recipeInstructions"].cast().toList(); + recipeInstructions = (data["recipeInstructions"] as List).cast(); } List tool = []; if (data["tool"] is Map) { - data["tool"].forEach((k, v) => tool.add(v as String)); + (data["tool"] as Map).forEach((k, v) => tool.add(v as String)); } else if (data["tool"] != null) { - tool = data["tool"].cast().toList(); + tool = (data["tool"] as List).cast(); } - int recipeYield = data["recipeYield"]; - Duration prepTime = data.containsKey("prepTime") && + final int recipeYield = data["recipeYield"] as int? ?? 1; + final Duration prepTime = data.containsKey("prepTime") && data["prepTime"] != "" && data["prepTime"] != null - ? IsoTimeFormat.toDuration(data["prepTime"]) - : null; - Duration cookTime = data.containsKey("cookTime") && + ? IsoTimeFormat.toDuration(data["prepTime"] as String) + : Duration.zero; + final Duration cookTime = data.containsKey("cookTime") && data["cookTime"] != "" && data["cookTime"] != null - ? IsoTimeFormat.toDuration(data["cookTime"]) - : null; - Duration totalTime = data.containsKey("totalTime") && + ? IsoTimeFormat.toDuration(data["cookTime"] as String) + : Duration.zero; + final Duration totalTime = data.containsKey("totalTime") && data["totalTime"] != "" && data["totalTime"] != null - ? IsoTimeFormat.toDuration(data["totalTime"]) - : null; - String keywords = data["keywords"]; - String image = data["image"]; - String url = data["url"]; + ? IsoTimeFormat.toDuration(data["totalTime"] as String) + : Duration.zero; + final String keywords = data["keywords"] as String? ?? ''; + final String image = data["image"] as String? ?? ''; + final String url = data["url"] as String? ?? ''; data.remove("id"); data.remove("name"); @@ -169,8 +131,50 @@ class Recipe extends Equatable { ); } + const Recipe._( + this.id, + this.name, + this.imageUrl, + this.recipeCategory, + this.description, + this.nutrition, + this.recipeIngredient, + this.recipeInstructions, + this.tool, + this.recipeYield, + this.prepTime, + this.cookTime, + this.totalTime, + this.keywords, + this.image, + this.url, + this.remainingData, + ); + + factory Recipe.empty() { + return Recipe._( + '0', + '', + '', + '', + '', + const {}, + List.empty(), + List.empty(), + List.empty(), + 1, + Duration.zero, + Duration.zero, + Duration.zero, + '', + '', + '', + const {}, + ); + } + String toJson() { - Map updatedData = { + final Map updatedData = { 'id': id, 'name': name, 'imageUrl': imageUrl, @@ -202,33 +206,33 @@ class Recipe extends Equatable { } MutableRecipe toMutableRecipe() { - MutableRecipe mutableRecipe = MutableRecipe(); + final MutableRecipe mutableRecipe = MutableRecipe(); - mutableRecipe.id = this.id; - mutableRecipe.name = this.name; - mutableRecipe.imageUrl = this.imageUrl; - mutableRecipe.recipeCategory = this.recipeCategory; - mutableRecipe.description = this.description; - mutableRecipe.nutrition = this.nutrition; - mutableRecipe.recipeIngredient = this.recipeIngredient; - mutableRecipe.recipeInstructions = this.recipeInstructions; - mutableRecipe.tool = this.tool; - mutableRecipe.recipeYield = this.recipeYield; - mutableRecipe.prepTime = this.prepTime; - mutableRecipe.cookTime = this.cookTime; - mutableRecipe.totalTime = this.totalTime; - mutableRecipe.keywords = this.keywords; - mutableRecipe.image = this.image; - mutableRecipe.url = this.url; - mutableRecipe.remainingData = this.remainingData; + mutableRecipe.id = id; + mutableRecipe.name = name; + mutableRecipe.imageUrl = imageUrl; + mutableRecipe.recipeCategory = recipeCategory; + mutableRecipe.description = description; + mutableRecipe.nutrition = nutrition; + mutableRecipe.recipeIngredient = recipeIngredient; + mutableRecipe.recipeInstructions = recipeInstructions; + mutableRecipe.tool = tool; + mutableRecipe.recipeYield = recipeYield; + mutableRecipe.prepTime = prepTime; + mutableRecipe.cookTime = cookTime; + mutableRecipe.totalTime = totalTime; + mutableRecipe.keywords = keywords; + mutableRecipe.image = image; + mutableRecipe.url = url; + mutableRecipe.remainingData = remainingData; return mutableRecipe; } @override - List get props => [id]; + List get props => [id]; - String _durationToIso(Duration duration) { + String _durationToIso(Duration? duration) { if (duration != null && duration.inMinutes != 0) { return "PT${duration.inHours}H${duration.inMinutes % 60}M"; } else { @@ -238,23 +242,23 @@ class Recipe extends Equatable { } class MutableRecipe { - int id; - String name; - String imageUrl; - String recipeCategory; - String description; - Map nutrition; - List recipeIngredient; - List recipeInstructions; - List tool; - int recipeYield; - Duration prepTime; - Duration cookTime; - Duration totalTime; - String keywords; - String image; - String url; - Map remainingData; + String id = '0'; + String name = ''; + String imageUrl = ''; + String recipeCategory = ''; + String description = ''; + Map nutrition = {}; + List recipeIngredient = []; + List recipeInstructions = []; + List tool = []; + int recipeYield = 0; + Duration prepTime = Duration.zero; + Duration cookTime = Duration.zero; + Duration totalTime = Duration.zero; + String keywords = ''; + String image = ''; + String url = ''; + Map remainingData = {}; Recipe toRecipe() { return Recipe._( diff --git a/lib/src/models/recipe_short.dart b/lib/src/models/recipe_short.dart index 01461fa1..7102cc1b 100644 --- a/lib/src/models/recipe_short.dart +++ b/lib/src/models/recipe_short.dart @@ -3,29 +3,31 @@ import 'dart:convert'; import 'package:equatable/equatable.dart'; class RecipeShort extends Equatable { - final int _recipeId; + final String _recipeId; final String _name; final String _imageUrl; - int get recipeId => _recipeId; + String get recipeId => _recipeId; String get name => _name; String get imageUrl => _imageUrl; RecipeShort.fromJson(Map json) : _recipeId = json["recipe_id"] is int - ? json["recipe_id"] - : int.parse(json["recipe_id"]), - _name = json["name"], - _imageUrl = json["imageUrl"]; + ? json["recipe_id"]!.toString() + : json["recipe_id"] as String, + _name = json["name"] as String, + _imageUrl = json["imageUrl"] as String; static List parseRecipesShort(String responseBody) { - final parsed = json.decode(responseBody).cast>(); + final parsed = json.decode(responseBody) as List; return parsed - .map((json) => RecipeShort.fromJson(json)) + .map( + (json) => RecipeShort.fromJson(json as Map), + ) .toList(); } @override - List get props => [_recipeId]; + List get props => [_recipeId]; } diff --git a/lib/src/models/timer.dart b/lib/src/models/timer.dart index 8042873d..cd93ec82 100644 --- a/lib/src/models/timer.dart +++ b/lib/src/models/timer.dart @@ -1,51 +1,45 @@ +import 'package:nextcloud_cookbook_flutter/src/services/notification_provider.dart'; import 'package:timezone/timezone.dart' as tz; -import '../services/notification_provider.dart'; - class TimerList { - static final TimerList _timerList = TimerList._internal(); - List timers = []; + static final TimerList _instance = TimerList._(); + final List timers; - factory TimerList() { - return _timerList; - } + factory TimerList() => _instance; - TimerList._internal(); + TimerList._() : timers = []; - List get(int recipeId) { - List l = []; - for (var value in this.timers) { + List get(String recipeId) { + final List l = []; + for (final value in timers) { if (value.recipeId == recipeId) l.add(value); } return l; } - clear() { - this.timers.clear(); + void clear() { + timers.clear(); NotificationService().cancelAll(); } } class Timer { - final String title; + final String? title; final String body; final Duration duration; - int id; - tz.TZDateTime done; - final int recipeId; + int id = 0; + final tz.TZDateTime done; + final String recipeId; Timer( this.recipeId, this.title, this.body, - this.duration, [ - tz.TZDateTime done, - ]) { - this.done = tz.TZDateTime.now(tz.local).add(this.duration); - } + this.duration, + ) : done = tz.TZDateTime.now(tz.local).add(duration); // Restore Timer fom pending notification - Timer.restore( + Timer._restore( this.recipeId, this.title, this.body, @@ -55,13 +49,16 @@ class Timer { ); factory Timer.fromJson(Map json, int id) { - Timer timer = Timer.restore( - json['recipeId'], - json['title'], - json['body'], - new Duration(minutes: json['duration']), - tz.TZDateTime.fromMicrosecondsSinceEpoch(tz.local, json['done']), - id); + final Timer timer = Timer._restore( + json['recipeId'] is String + ? json['recipeId'] as String + : json['recipeId'].toString(), + json['title'] as String, + json['body'] as String, + Duration(minutes: json['duration'] as int), + tz.TZDateTime.fromMicrosecondsSinceEpoch(tz.local, json['done'] as int), + id, + ); TimerList().timers.add(timer); return timer; } @@ -75,29 +72,28 @@ class Timer { 'recipeId': recipeId, }; - start() async { + void start() { NotificationService().start(this); } // cancel the timer - cancel() { + void cancel() { NotificationService().cancel(this); TimerList().timers.remove(this); } Duration remaining() { - if(this.done.difference(tz.TZDateTime.now(tz.local)).isNegative){ - return Duration.zero; - } - else{ - return this.done.difference(tz.TZDateTime.now(tz.local)); - } + if (done.difference(tz.TZDateTime.now(tz.local)).isNegative) { + return Duration.zero; + } else { + return done.difference(tz.TZDateTime.now(tz.local)); + } } double progress() { - Duration remainingTime = remaining(); + final Duration remainingTime = remaining(); return remainingTime.inSeconds > 0 - ? 1 - (remainingTime.inSeconds / this.duration.inSeconds) + ? 1 - (remainingTime.inSeconds / duration.inSeconds) : 1.0; } } diff --git a/lib/src/screens/category/category_screen.dart b/lib/src/screens/category/category_screen.dart index 167337fa..a93495c5 100644 --- a/lib/src/screens/category/category_screen.dart +++ b/lib/src/screens/category/category_screen.dart @@ -10,6 +10,7 @@ import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/my_settings_screen.dart'; +import 'package:nextcloud_cookbook_flutter/src/screens/recipe/recipe_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe_create_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe_import_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipes_list_screen.dart'; @@ -21,9 +22,9 @@ import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_netw import 'package:nextcloud_cookbook_flutter/src/widget/category_card.dart'; import 'package:search_page/search_page.dart'; -import '../recipe/recipe_screen.dart'; - class CategoryScreen extends StatefulWidget { + const CategoryScreen({super.key}); + @override _CategoryScreenState createState() => _CategoryScreenState(); } @@ -45,7 +46,7 @@ class _CategoryScreenState extends State { ), ); }, - child: Icon(Icons.add), + child: const Icon(Icons.add), ), drawer: Drawer( child: ListView( @@ -53,6 +54,9 @@ class _CategoryScreenState extends State { padding: EdgeInsets.zero, children: [ DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + ), child: Center( child: ClipOval( child: AuthenticationCachedNetworkImage( @@ -61,9 +65,6 @@ class _CategoryScreenState extends State { ), ), ), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - ), ), ListTile( trailing: Icon( @@ -75,9 +76,11 @@ class _CategoryScreenState extends State { Navigator.pop(context); Navigator.push( context, - MaterialPageRoute(builder: (context) { - return TimerScreen(); - }), + MaterialPageRoute( + builder: (context) { + return const TimerScreen(); + }, + ), ); }, ), @@ -91,9 +94,11 @@ class _CategoryScreenState extends State { Navigator.pop(context); Navigator.push( context, - MaterialPageRoute(builder: (context) { - return RecipeImportScreen(); - }), + MaterialPageRoute( + builder: (context) { + return const RecipeImportScreen(); + }, + ), ); }, ), @@ -106,9 +111,11 @@ class _CategoryScreenState extends State { onTap: () async { await Navigator.push( context, - MaterialPageRoute(builder: (context) { - return MySettingsScreen(); - }), + MaterialPageRoute( + builder: (context) { + return const MySettingsScreen(); + }, + ), ); setState(() {}); }, @@ -140,7 +147,7 @@ class _CategoryScreenState extends State { delegate: SearchPage( items: recipeShortState.recipesShort, searchLabel: translate('search.title'), - suggestion: Center( + suggestion: const Center( // child: Text('Filter people by name, surname or age'), ), failure: Center( @@ -151,12 +158,10 @@ class _CategoryScreenState extends State { ], builder: (recipe) => ListTile( title: Text(recipe.name), - trailing: Container( - child: AuthenticationCachedNetworkRecipeImage( - recipeId: recipe.recipeId, - full: false, - width: 50, - ), + trailing: AuthenticationCachedNetworkRecipeImage( + recipeId: recipe.recipeId, + full: false, + width: 50, ), onTap: () => Navigator.of(context).pushReplacement( @@ -219,57 +224,63 @@ class _CategoryScreenState extends State { ), ], ), - body: RefreshIndicator(onRefresh: () { - DefaultCacheManager().emptyCache(); - BlocProvider.of(context).add(CategoriesLoaded()); - return Future.value(true); - }, child: () { - if (categoriesState is CategoriesLoadSuccess) { - return _buildCategoriesScreen(categoriesState.categories); - } else if (categoriesState is CategoriesImageLoadSuccess) { - return _buildCategoriesScreen(categoriesState.categories); - } else if (categoriesState is CategoriesLoadInProgress || - categoriesState is CategoriesInitial) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: SpinKitWave( - color: Theme.of(context).primaryColor, - size: 50.0, - ), - ), - ApiVersionWarning(), - ], - ); - } else if (categoriesState is CategoriesLoadFailure) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( + body: RefreshIndicator( + onRefresh: () { + DefaultCacheManager().emptyCache(); + BlocProvider.of(context).add(CategoriesLoaded()); + return Future.value(); + }, + child: () { + if (categoriesState is CategoriesLoadSuccess) { + return _buildCategoriesScreen(categoriesState.categories); + } else if (categoriesState is CategoriesImageLoadSuccess) { + return _buildCategoriesScreen(categoriesState.categories); + } else if (categoriesState is CategoriesLoadInProgress || + categoriesState is CategoriesInitial) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - translate('categories.errors.plugin_missing'), - style: TextStyle(fontWeight: FontWeight.bold), + Center( + child: SpinKitWave( + color: Theme.of(context).primaryColor, + ), ), - Divider(), - Text(translate('categories.errors.load_failed', - args: {'error_msg': categoriesState.errorMsg})), + const ApiVersionWarning(), ], - ), - ); - } else { - return Text(translate('categories.errors.unknown')); - } - }()), + ); + } else if (categoriesState is CategoriesLoadFailure) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + translate('categories.errors.plugin_missing'), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const Divider(), + Text( + translate( + 'categories.errors.load_failed', + args: {'error_msg': categoriesState.errorMsg}, + ), + ), + ], + ), + ); + } else { + return Text(translate('categories.errors.unknown')); + } + }(), + ), ); }, ); } Widget _buildCategoriesScreen(List categories) { - double screenWidth = MediaQuery.of(context).size.width; - int axisRatio = (screenWidth / 150).round(); - int axisCount = axisRatio < 1 ? 1 : axisRatio; + final double screenWidth = MediaQuery.of(context).size.width; + final int axisRatio = (screenWidth / 150).round(); + final int axisCount = axisRatio < 1 ? 1 : axisRatio; return Padding( padding: const EdgeInsets.all(10.0), @@ -277,7 +288,7 @@ class _CategoryScreenState extends State { crossAxisCount: axisCount, crossAxisSpacing: 10, mainAxisSpacing: 10, - padding: EdgeInsets.only(top: 10), + padding: const EdgeInsets.only(top: 10), semanticChildCount: categories.length, children: categories .map( diff --git a/lib/src/screens/form/login_form.dart b/lib/src/screens/form/login_form.dart index a97da7a5..df2dc191 100644 --- a/lib/src/screens/form/login_form.dart +++ b/lib/src/screens/form/login_form.dart @@ -4,13 +4,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/login/login.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/checkbox_form_field.dart'; import 'package:punycode/punycode.dart'; -import '../../blocs/login/login.dart'; - class LoginForm extends StatefulWidget { + const LoginForm({super.key}); + @override State createState() => _LoginFormState(); } @@ -29,7 +30,7 @@ class _LoginFormState extends State with WidgetsBindingObserver { // not a GlobalKey. final _formKey = GlobalKey(); - Function authenticateInterruptCallback; + late Function() authenticateInterruptCallback; @override void didChangeAppLifecycleState(AppLifecycleState state) { @@ -39,7 +40,7 @@ class _LoginFormState extends State with WidgetsBindingObserver { } @override - initState() { + void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @@ -56,19 +57,18 @@ class _LoginFormState extends State with WidgetsBindingObserver { UserRepository().stopAuthenticate(); }; - _onLoginButtonPressed() { - _formKey.currentState.save(); - - if (_formKey.currentState.validate()) { - String serverUrl = _punyEncodeUrl(_serverUrl.text); - String username = _username.text.trim(); - String password = _password.text.trim(); - String originalBasicAuth = 'Basic ' + - base64Encode( - utf8.encode( - '$username:$password', - ), - ); + void onLoginButtonPressed() { + _formKey.currentState?.save(); + + if (_formKey.currentState?.validate() ?? false) { + final String serverUrl = _punyEncodeUrl(_serverUrl.text); + final String username = _username.text.trim(); + final String password = _password.text.trim(); + final String originalBasicAuth = 'Basic ${base64Encode( + utf8.encode( + '$username:$password', + ), + )}'; BlocProvider.of(context).add( LoginButtonPressed( serverURL: serverUrl, @@ -86,7 +86,7 @@ class _LoginFormState extends State with WidgetsBindingObserver { if (state is LoginFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('${state.error}'), + content: Text(state.error), backgroundColor: Colors.red, ), ); @@ -110,23 +110,28 @@ class _LoginFormState extends State with WidgetsBindingObserver { controller: _serverUrl, keyboardType: TextInputType.url, validator: (value) { - if (value.isEmpty) { + if (value == null || value.isEmpty) { return translate( - 'login.server_url.validator.empty'); + 'login.server_url.validator.empty', + ); } - var urlPattern = + const urlPattern = r"^(?:http(s)?:\/\/)?[\w.-]+(?:(?:\.[\w\.-]+)|(?:\:\d+))+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*$"; - bool _match = - new RegExp(urlPattern, caseSensitive: false) + final bool match = + RegExp(urlPattern, caseSensitive: false) .hasMatch(_punyEncodeUrl(value)); - if (!_match) { + if (!match) { return translate( - 'login.server_url.validator.pattern'); + 'login.server_url.validator.pattern', + ); } return null; }, textInputAction: TextInputAction.next, - autofillHints: [AutofillHints.url, AutofillHints.name], + autofillHints: const [ + AutofillHints.url, + AutofillHints.name + ], ), TextFormField( decoration: InputDecoration( @@ -134,7 +139,7 @@ class _LoginFormState extends State with WidgetsBindingObserver { ), controller: _username, textInputAction: TextInputAction.next, - autofillHints: [AutofillHints.username], + autofillHints: const [AutofillHints.username], ), TextFormField( decoration: InputDecoration( @@ -144,16 +149,16 @@ class _LoginFormState extends State with WidgetsBindingObserver { obscureText: true, onFieldSubmitted: (val) { if (state is! LoginLoading) { - _onLoginButtonPressed(); + onLoginButtonPressed(); } }, textInputAction: TextInputAction.done, - autofillHints: [AutofillHints.password], + autofillHints: const [AutofillHints.password], ), Padding( padding: const EdgeInsets.only(top: 16.0), child: ExpansionPanelList( - expandedHeaderPadding: const EdgeInsets.all(0), + expandedHeaderPadding: EdgeInsets.zero, expansionCallback: (int index, bool isExpanded) { setState(() { advancedSettingsExpanded = !isExpanded; @@ -168,25 +173,34 @@ class _LoginFormState extends State with WidgetsBindingObserver { children: [ CheckboxFormField( initialValue: advancedIsAppPassword, - onSaved: (bool checked) => { + onSaved: (bool? checked) { + if (checked == null) return; setState(() { advancedIsAppPassword = checked; - }) + }); }, - title: Text(translate( - 'login.settings.app_password')), + title: Text( + translate( + 'login.settings.app_password', + ), + ), ), CheckboxFormField( initialValue: advancedIsSelfSignedCertificate, - onSaved: (bool checked) => { + onSaved: (bool? checked) { + if (checked == null) return; + setState(() { advancedIsSelfSignedCertificate = checked; - }) + }); }, - title: Text(translate( - 'login.settings.self_signed_certificate')), + title: Text( + translate( + 'login.settings.self_signed_certificate', + ), + ), ), ], ), @@ -208,7 +222,7 @@ class _LoginFormState extends State with WidgetsBindingObserver { ), ElevatedButton( onPressed: state is! LoginLoading - ? _onLoginButtonPressed + ? onLoginButtonPressed : null, child: Text(translate('login.button')), ), @@ -216,7 +230,7 @@ class _LoginFormState extends State with WidgetsBindingObserver { child: state is LoginLoading ? SpinKitWave( color: Theme.of(context).primaryColor, - size: 50.0) + ) : null, ), ], @@ -230,21 +244,22 @@ class _LoginFormState extends State with WidgetsBindingObserver { ); } - String _punyEncodeUrl(String url) { - String pattern = r"(?:\.|^)([^.]*?[^\x00-\x7F][^.]*?)(?:\.|$)"; - RegExp expression = new RegExp(pattern, caseSensitive: false); + String _punyEncodeUrl(String punycodeUrl) { + const String pattern = r"(?:\.|^)([^.]*?[^\x00-\x7F][^.]*?)(?:\.|$)"; + final RegExp expression = RegExp(pattern, caseSensitive: false); String prefix = ""; - if (url.startsWith("https://")) { - url = url.replaceFirst("https://", ""); + String url = punycodeUrl; + if (punycodeUrl.startsWith("https://")) { + url = punycodeUrl.replaceFirst("https://", ""); prefix = "https://"; - } else if (url.startsWith("http://")) { - url = url.replaceFirst("http://", ""); + } else if (punycodeUrl.startsWith("http://")) { + url = punycodeUrl.replaceFirst("http://", ""); prefix = "http://"; } while (expression.hasMatch(url)) { - String match = expression.firstMatch(url).group(1); - url = url.replaceFirst(match, "xn--" + punycodeEncode(match)); + final String match = expression.firstMatch(url)!.group(1)!; + url = url.replaceFirst(match, "xn--${punycodeEncode(match)}"); } return prefix + url; diff --git a/lib/src/screens/form/recipe_form.dart b/lib/src/screens/form/recipe_form.dart index c9e64947..9c9505da 100644 --- a/lib/src/screens/form/recipe_form.dart +++ b/lib/src/screens/form/recipe_form.dart @@ -11,14 +11,21 @@ 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'; typedef RecipeFormSubmit = void Function( - MutableRecipe mutableRecipe, BuildContext context); + MutableRecipe mutableRecipe, + BuildContext context, +); class RecipeForm extends StatefulWidget { final Recipe recipe; final String buttonSubmitText; final RecipeFormSubmit recipeFormSubmit; - const RecipeForm(this.recipe, this.buttonSubmitText, this.recipeFormSubmit); + const RecipeForm( + this.recipe, + this.buttonSubmitText, + this.recipeFormSubmit, { + super.key, + }); @override _RecipeFormState createState() => _RecipeFormState(); @@ -26,9 +33,9 @@ class RecipeForm extends StatefulWidget { class _RecipeFormState extends State { final _formKey = GlobalKey(); - Recipe recipe; - MutableRecipe _mutableRecipe; - TextEditingController categoryController; + late Recipe recipe; + late MutableRecipe _mutableRecipe; + late TextEditingController categoryController; @override void initState() { @@ -55,13 +62,13 @@ class _RecipeFormState extends State { children: [ Text( translate('recipe.fields.name'), - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), TextFormField( - enabled: !(state is RecipeUpdateInProgress), + enabled: state is! RecipeUpdateInProgress, initialValue: recipe.name, onChanged: (value) { _mutableRecipe.name = value; @@ -74,13 +81,13 @@ class _RecipeFormState extends State { children: [ Text( translate('recipe.fields.description'), - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), TextFormField( - enabled: !(state is RecipeUpdateInProgress), + enabled: state is! RecipeUpdateInProgress, initialValue: recipe.description, maxLines: 100, minLines: 1, @@ -95,7 +102,7 @@ class _RecipeFormState extends State { children: [ Text( translate('recipe.fields.category'), - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), @@ -103,22 +110,25 @@ class _RecipeFormState extends State { TypeAheadFormField( getImmediateSuggestions: true, textFieldConfiguration: TextFieldConfiguration( - controller: this.categoryController, + controller: categoryController, ), - suggestionsCallback: (pattern) async { - return await DataRepository() - .getMatchingCategoryNames(pattern); + suggestionsCallback: + DataRepository().getMatchingCategoryNames, + itemBuilder: (context, String? suggestion) { + if (suggestion != null) { + return ListTile( + title: Text(suggestion), + ); + } + return const SizedBox(); }, - itemBuilder: (context, String suggestion) { - return ListTile( - title: Text(suggestion), - ); - }, - onSuggestionSelected: (String suggestion) { - this.categoryController.text = suggestion; + onSuggestionSelected: (String? suggestion) { + if (suggestion == null) return; + categoryController.text = suggestion; }, onSaved: (value) { - this._mutableRecipe.recipeCategory = value; + if (value == null) return; + _mutableRecipe.recipeCategory = value; }, ) ], @@ -128,13 +138,13 @@ class _RecipeFormState extends State { children: [ Text( translate('recipe.fields.keywords'), - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), TextFormField( - enabled: !(state is RecipeUpdateInProgress), + enabled: state is! RecipeUpdateInProgress, initialValue: recipe.keywords, onChanged: (value) { _mutableRecipe.keywords = value; @@ -147,13 +157,13 @@ class _RecipeFormState extends State { children: [ Text( translate('recipe.fields.source'), - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), TextFormField( - enabled: !(state is RecipeUpdateInProgress), + enabled: state is! RecipeUpdateInProgress, initialValue: recipe.url, onChanged: (value) { _mutableRecipe.url = value; @@ -166,14 +176,14 @@ class _RecipeFormState extends State { children: [ Text( translate('recipe.fields.image'), - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), TextFormField( enabled: false, - style: TextStyle(color: Colors.grey), + style: const TextStyle(color: Colors.grey), initialValue: recipe.imageUrl, onChanged: (value) { _mutableRecipe.imageUrl = value; @@ -186,13 +196,13 @@ class _RecipeFormState extends State { children: [ Text( translate('recipe.fields.servings'), - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), IntegerTextFormField( - enabled: !(state is RecipeUpdateInProgress), + enabled: state is! RecipeUpdateInProgress, initialValue: recipe.recipeYield, onChanged: (value) => _mutableRecipe.recipeYield = value, onSaved: (value) => _mutableRecipe.recipeYield = value, @@ -236,25 +246,30 @@ class _RecipeFormState extends State { onSave: (value) => {_mutableRecipe.recipeInstructions = value}, ), - Container( + SizedBox( width: 150, child: ElevatedButton( onPressed: () { - if (_formKey.currentState.validate()) { - _formKey.currentState.save(); + if (_formKey.currentState?.validate() ?? false) { + _formKey.currentState?.save(); widget.recipeFormSubmit(_mutableRecipe, context); } }, child: () { - if (state is RecipeUpdateInProgress) { - return SpinKitWave(color: Colors.white, size: 30.0); - } else if (state is RecipeUpdateFailure || - state is RecipeUpdateSuccess || - state is RecipeLoadSuccess || - state is RecipeCreateSuccess || - state is RecipeCreateFailure || - state is RecipeInitial) { - return Text(widget.buttonSubmitText); + switch (state.runtimeType) { + case RecipeUpdateInProgress: + return const SpinKitWave( + color: Colors.white, + size: 30.0, + ); + case RecipeUpdateFailure: + case RecipeUpdateSuccess: + case RecipeLoadSuccess: + case RecipeCreateSuccess: + case RecipeCreateFailure: + case RecipeInitial: + default: + return Text(widget.buttonSubmitText); } }(), ), diff --git a/lib/src/screens/form/recipe_import_form.dart b/lib/src/screens/form/recipe_import_form.dart index 2a183bf6..785ea033 100644 --- a/lib/src/screens/form/recipe_import_form.dart +++ b/lib/src/screens/form/recipe_import_form.dart @@ -9,14 +9,14 @@ import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; class RecipeImportForm extends StatefulWidget { final String importUrl; - RecipeImportForm([this.importUrl = '']); + const RecipeImportForm([this.importUrl = '']); @override _RecipeImportFormState createState() => _RecipeImportFormState(); } class _RecipeImportFormState extends State { - var _importUrlController = TextEditingController(); + final _importUrlController = TextEditingController(); @override void initState() { @@ -34,60 +34,66 @@ class _RecipeImportFormState extends State { @override Widget build(BuildContext context) { return BlocBuilder( - builder: (BuildContext context, RecipeState state) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Form( - child: Column( - children: [ - TextField( - enabled: state is RecipeImportInProgress ? false : true, - controller: _importUrlController, - decoration: InputDecoration( - hintText: translate("recipe_import.field"), - suffixIcon: IconButton( - tooltip: translate("recipe_import.clipboard"), - onPressed: () async => { - _importUrlController.text = - (await Clipboard.getData('text/plain')).text - }, - icon: Icon(Icons.content_copy), + builder: (BuildContext context, RecipeState state) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Form( + child: Column( + children: [ + TextField( + enabled: state is! RecipeImportInProgress, + controller: _importUrlController, + decoration: InputDecoration( + hintText: translate("recipe_import.field"), + suffixIcon: IconButton( + tooltip: translate("recipe_import.clipboard"), + onPressed: () async { + final clipboard = + await Clipboard.getData('text/plain'); + final text = clipboard?.text; + if (text != null) _importUrlController.text = text; + }, + icon: const Icon(Icons.content_copy), + ), ), ), - ), - Center( - child: TextButton( - onPressed: () => { - state is! RecipeImportInProgress - ? BlocProvider.of(context) + Center( + child: TextButton( + onPressed: () => { + if (state is! RecipeImportInProgress) + BlocProvider.of(context) .add(RecipeImported(_importUrlController.text)) - : null - }, - child: () { - return state is RecipeImportInProgress - ? SpinKitWave( - color: Theme.of(context).primaryColor, size: 30.0) - : Row( - children: [ - Spacer(), - Padding( - padding: const EdgeInsets.only(right: 9.0), - child: - Text(translate("recipe_import.button")), - ), - Icon(Icons.cloud_download_outlined), - Spacer(), - ], - ); - }(), - ), - ) - ], + else + null + }, + child: () { + return state is RecipeImportInProgress + ? SpinKitWave( + color: Theme.of(context).primaryColor, + size: 30.0, + ) + : Row( + children: [ + const Spacer(), + Padding( + padding: const EdgeInsets.only(right: 9.0), + child: + Text(translate("recipe_import.button")), + ), + const Icon(Icons.cloud_download_outlined), + const Spacer(), + ], + ); + }(), + ), + ) + ], + ), ), ), - ), - ); - }); + ); + }, + ); } } diff --git a/lib/src/screens/loading_screen.dart b/lib/src/screens/loading_screen.dart index 0aff9fd8..369bd72b 100644 --- a/lib/src/screens/loading_screen.dart +++ b/lib/src/screens/loading_screen.dart @@ -5,6 +5,8 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication.dart'; class LoadingScreen extends StatelessWidget { + const LoadingScreen({super.key}); + @override Widget build(BuildContext context) { return Scaffold( @@ -13,32 +15,32 @@ class LoadingScreen extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ - if (!(authenticationState is AuthenticationError)) + if (authenticationState is! AuthenticationError) SpinKitWave( color: Theme.of(context).primaryColor, - size: 50.0, ), if (authenticationState is AuthenticationError) Text(authenticationState.errorMsg), - SizedBox(height: 10), + const SizedBox(height: 10), if (authenticationState is AuthenticationError) ElevatedButton( - onPressed: () { - BlocProvider.of(context) - .add(AppStarted()); - }, - child: Text(translate("login.retry"))), - SizedBox(height: 10), + onPressed: () { + BlocProvider.of(context) + .add(AppStarted()); + }, + child: Text(translate("login.retry")), + ), + const SizedBox(height: 10), if (authenticationState is AuthenticationError) ElevatedButton( - onPressed: () { - BlocProvider.of(context) - .add(LoggedOut()); - }, - child: Text(translate("login.reset"))), + onPressed: () { + BlocProvider.of(context) + .add(LoggedOut()); + }, + child: Text(translate("login.reset")), + ), ], ), ); diff --git a/lib/src/screens/login_screen.dart b/lib/src/screens/login_screen.dart index 3d9616aa..27cf57ce 100644 --- a/lib/src/screens/login_screen.dart +++ b/lib/src/screens/login_screen.dart @@ -2,13 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../blocs/authentication/authentication_bloc.dart'; -import '../blocs/login/login_bloc.dart'; -import 'form/login_form.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; +import 'package:nextcloud_cookbook_flutter/src/blocs/login/login_bloc.dart'; +import 'package:nextcloud_cookbook_flutter/src/screens/form/login_form.dart'; class LoginScreen extends StatelessWidget { final bool invalidCredentials; - LoginScreen({this.invalidCredentials = false}); + const LoginScreen({ + super.key, + this.invalidCredentials = false, + }); @override Widget build(BuildContext context) { @@ -25,12 +28,12 @@ class LoginScreen extends StatelessWidget { authenticationBloc: BlocProvider.of(context), ); }, - child: LoginForm(), + child: const LoginForm(), ), ); } - void notifyIfInvalidCredentials(context) { + void notifyIfInvalidCredentials(BuildContext context) { if (invalidCredentials) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/src/screens/my_settings_screen.dart b/lib/src/screens/my_settings_screen.dart index 04c4c3f6..d4d9e396 100644 --- a/lib/src/screens/my_settings_screen.dart +++ b/lib/src/screens/my_settings_screen.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:flutter_translate/flutter_translate.dart'; @@ -9,7 +8,7 @@ import 'package:nextcloud_cookbook_flutter/src/util/supported_locales.dart'; import 'package:theme_mode_handler/theme_mode_handler.dart'; class MySettingsScreen extends StatefulWidget { - const MySettingsScreen({Key key}) : super(key: key); + const MySettingsScreen({super.key}); @override State createState() => _MySettingsScreenState(); @@ -23,15 +22,13 @@ class _MySettingsScreenState extends State { children: [ SwitchSettingsTile( title: translate("settings.stay_awake.title"), - settingKey: describeEnum( - SettingKeys.stay_awake, - ), + settingKey: SettingKeys.stay_awake.name, subtitle: translate("settings.stay_awake.subtitle"), ), SliderSettingsTile( title: translate("settings.recipe_font_size.title"), - settingKey: describeEnum(SettingKeys.recipe_font_size), - defaultValue: Theme.of(context).textTheme.bodyText2.fontSize, + settingKey: SettingKeys.recipe_font_size.name, + defaultValue: Theme.of(context).textTheme.bodyText2!.fontSize!, min: 10, max: 25, eagerUpdate: false, @@ -39,7 +36,7 @@ class _MySettingsScreenState extends State { ), SliderSettingsTile( title: translate("settings.category_font_size.title"), - settingKey: describeEnum(SettingKeys.category_font_size), + settingKey: SettingKeys.category_font_size.name, defaultValue: 16, min: 10, max: 25, @@ -48,38 +45,38 @@ class _MySettingsScreenState extends State { ), DropDownSettingsTile( title: translate("settings.dark_mode.title"), - settingKey: describeEnum(SettingKeys.dark_mode), + 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(), + 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); + ThemeModeHandler.of(context)?.saveThemeMode(theme); }, ), DropDownSettingsTile( title: translate("settings.language.title"), - settingKey: describeEnum(SettingKeys.language), + settingKey: SettingKeys.language.name, selected: Settings.getValue( - describeEnum(SettingKeys.language), - 'default', + SettingKeys.language.name, + defaultValue: 'default', ), values: Map.from( { 'default': translate("settings.dark_mode.system"), }, )..addAll(SupportedLocales.locales), - onChange: (value) { + onChange: (dynamic value) { if (value == 'default') { changeLocale(context, Platform.localeName); } else { - changeLocale(context, value); + changeLocale(context, value as String?); } setState(() {}); }, diff --git a/lib/src/screens/recipe/recipe_screen.dart b/lib/src/screens/recipe/recipe_screen.dart index 2e756d34..69bc5594 100644 --- a/lib/src/screens/recipe/recipe_screen.dart +++ b/lib/src/screens/recipe/recipe_screen.dart @@ -1,7 +1,5 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:flutter_translate/flutter_translate.dart'; @@ -16,24 +14,26 @@ import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/animated_time_progress_bar.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/duration_indicator.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:wakelock/wakelock.dart'; class RecipeScreen extends StatefulWidget { - final int recipeId; + final String recipeId; - const RecipeScreen({Key key, @required this.recipeId}) : super(key: key); + const RecipeScreen({ + super.key, + required this.recipeId, + }); @override State createState() => RecipeScreenState(); } class RecipeScreenState extends State { - int recipeId; bool isLargeScreen = false; Future _disableWakelock() async { - bool wakelockEnabled = await Wakelock.enabled; + final bool wakelockEnabled = await Wakelock.enabled; if (wakelockEnabled) { Wakelock.disable(); } @@ -41,7 +41,10 @@ class RecipeScreenState extends State { } void _enableWakelock() { - if (Settings.getValue(describeEnum(SettingKeys.stay_awake), false)) { + if (Settings.getValue( + SettingKeys.stay_awake.name, + defaultValue: false, + )!) { Wakelock.enable(); } } @@ -49,98 +52,87 @@ class RecipeScreenState extends State { @override void initState() { _enableWakelock(); - recipeId = widget.recipeId; super.initState(); } - Future _refresh() async { - DefaultCacheManager().emptyCache(); - this.setState(() {}); - return Future.value(true); - } - @override Widget build(BuildContext context) { - TextStyle settingsBasedTextStyle = TextStyle( - fontSize: Settings.getValue( - describeEnum(SettingKeys.recipe_font_size), - Theme.of(context).textTheme.bodyText2.fontSize, - ), - ); - - this.isLargeScreen = MediaQuery.of(context).size.width > 600; + isLargeScreen = MediaQuery.of(context).size.width > 600; return BlocProvider( - create: (context) => RecipeBloc()..add(RecipeLoaded(recipeId: recipeId)), + create: (context) => RecipeBloc()..add(RecipeLoaded(widget.recipeId)), child: BlocBuilder( - builder: (BuildContext context, RecipeState state) { - final recipeBloc = BlocProvider.of(context); - return WillPopScope( - onWillPop: () => _disableWakelock(), - child: Scaffold( - appBar: AppBar( - title: Text(translate('recipe.title')), - actions: [ - // action button - IconButton( - icon: Icon( - Icons.edit, - ), - onPressed: () async { - if (state is RecipeLoadSuccess) { - _disableWakelock(); - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return BlocProvider.value( + builder: (BuildContext context, RecipeState state) { + final recipeBloc = BlocProvider.of(context); + return WillPopScope( + onWillPop: () => _disableWakelock(), + child: Scaffold( + appBar: AppBar( + title: Text(translate('recipe.title')), + actions: [ + // action button + IconButton( + icon: const Icon( + Icons.edit, + ), + onPressed: () async { + if (state is RecipeLoadSuccess) { + _disableWakelock(); + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return BlocProvider.value( value: recipeBloc, - child: RecipeEditScreen(state.recipe)); - }, - ), - ); - _enableWakelock(); - } - }, - ), - ], + child: RecipeEditScreen(state.recipe), + ); + }, + ), + ); + _enableWakelock(); + } + }, + ), + ], + ), + floatingActionButton: state is RecipeLoadSuccess + ? _buildFabButton(state.recipe) + : null, + body: () { + if (state is RecipeLoadSuccess) { + return _buildRecipeScreen(state.recipe); + } else if (state is RecipeLoadInProgress) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (state is RecipeFailure) { + return Center( + child: Text(state.errorMsg), + ); + } else { + return const Center( + child: Text("FAILED"), + ); + } + }(), ), - floatingActionButton: state is RecipeLoadSuccess - ? _buildFabButton(state.recipe) - : null, - body: () { - if (state is RecipeLoadSuccess) { - return _buildRecipeScreen(state.recipe); - } else if (state is RecipeLoadInProgress) { - return Center( - child: CircularProgressIndicator(), - ); - } else if (state is RecipeFailure) { - return Center( - child: Text(state.errorMsg), - ); - } else { - return Center( - child: Text("FAILED"), - ); - } - }(), - ), - ); - }), + ); + }, + ), ); } FloatingActionButton _buildFabButton(Recipe recipe) { - var enabled = recipe.cookTime != null && recipe.cookTime > Duration.zero; + final enabled = recipe.cookTime > Duration.zero; return FloatingActionButton( onPressed: () { { if (enabled) { - Timer timer = new Timer( - recipe.id, - recipe.name, - recipe.name + " " + translate('timer.finished'), - recipe.cookTime); + final Timer timer = Timer( + recipe.id, + recipe.name, + "${recipe.name} ${translate('timer.finished')}", + recipe.cookTime, + ); timer.start(); TimerList().timers.add(timer); setState(() {}); @@ -154,26 +146,28 @@ class RecipeScreenState extends State { } } }, - child: Icon(Icons.access_alarm), backgroundColor: enabled - ? Theme.of(context).accentColor + ? Theme.of(context).colorScheme.secondary : Theme.of(context).disabledColor, + child: const Icon(Icons.access_alarm), ); } Widget _buildRecipeScreen(Recipe recipe) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - TextStyle settingsBasedTextStyle = TextStyle( + final TextStyle settingsBasedTextStyle = TextStyle( fontSize: Settings.getValue( - describeEnum(SettingKeys.recipe_font_size), - Theme.of(context).textTheme.bodyText2.fontSize, + SettingKeys.recipe_font_size.name, + defaultValue: Theme.of(context).textTheme.bodyText2?.fontSize, ), ); return ListView( children: [ - Container( + SizedBox( + width: double.infinity, + height: 200, child: Center( child: AuthenticationCachedNetworkRecipeImage( recipeId: recipe.id, @@ -183,21 +177,21 @@ class RecipeScreenState extends State { boxFit: BoxFit.cover, ), ), - width: double.infinity, - height: 200, ), Padding( padding: const EdgeInsets.all(16.0), child: ListView( shrinkWrap: true, - physics: ClampingScrollPhysics(), + physics: const ClampingScrollPhysics(), children: [ Padding( padding: const EdgeInsets.only(bottom: 15.0), child: Text( recipe.name, - style: - TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), ), ), Padding( @@ -215,26 +209,26 @@ class RecipeScreenState extends State { text: translate('recipe.fields.servings'), style: Theme.of(context) .textTheme - .bodyText2 + .bodyText2! .apply(fontWeightDelta: 3), children: [ TextSpan( - text: " " + recipe.recipeYield.toString(), + text: " ${recipe.recipeYield}", style: Theme.of(context) .textTheme - .bodyText2 + .bodyText2! .apply(fontWeightDelta: 3), ) ], ), ), - Spacer(), + const Spacer(), if (recipe.url.isNotEmpty) ElevatedButton( - style: ButtonStyle(), + style: const ButtonStyle(), onPressed: () async { - if (await canLaunch(recipe.url)) { - await launch(recipe.url); + if (await launchUrlString(recipe.url)) { + await launchUrlString(recipe.url); } }, child: @@ -250,18 +244,21 @@ class RecipeScreenState extends State { runSpacing: 10, spacing: 10, children: [ - if (recipe.prepTime != null) + if (recipe.prepTime > Duration.zero) DurationIndicator( - duration: recipe.prepTime, - name: translate('recipe.prep')), - if (recipe.cookTime != null) + duration: recipe.prepTime, + name: translate('recipe.prep'), + ), + if (recipe.cookTime > Duration.zero) DurationIndicator( - duration: recipe.cookTime, - name: translate('recipe.cook')), - if (recipe.totalTime != null) + duration: recipe.cookTime, + name: translate('recipe.cook'), + ), + if (recipe.totalTime > Duration.zero) DurationIndicator( - duration: recipe.totalTime, - name: translate('recipe.total')), + duration: recipe.totalTime, + name: translate('recipe.total'), + ), ], ), ), @@ -282,7 +279,7 @@ class RecipeScreenState extends State { child: Text( recipe.tool.fold( "", - (p, e) => p + "- " + e.trim() + "\n", + (p, e) => "$p- ${e.trim()}\n", ), style: settingsBasedTextStyle, ), @@ -292,37 +289,46 @@ class RecipeScreenState extends State { ), ), ), - if (this.isLargeScreen && recipe.recipeIngredient.isNotEmpty) + if (isLargeScreen && recipe.recipeIngredient.isNotEmpty) Padding( - padding: const EdgeInsets.only(bottom: 10.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 5, - child: NutritionList(recipe.nutrition), - ), - Expanded( - flex: 5, - child: IngredientList( - recipe, settingsBasedTextStyle), - ), - Expanded( - flex: 5, - child: InstructionList( - recipe, settingsBasedTextStyle), - ) - ])) + padding: const EdgeInsets.only(bottom: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 5, + child: NutritionList(recipe.nutrition), + ), + Expanded( + flex: 5, + child: IngredientList( + recipe, + settingsBasedTextStyle, + ), + ), + Expanded( + flex: 5, + child: InstructionList( + recipe, + settingsBasedTextStyle, + ), + ) + ], + ), + ) else Padding( - padding: const EdgeInsets.only(bottom: 10.0), - child: Column(children: [ + padding: const EdgeInsets.only(bottom: 10.0), + child: Column( + children: [ if (recipe.nutrition.isNotEmpty) NutritionList(recipe.nutrition), if (recipe.recipeIngredient.isNotEmpty) IngredientList(recipe, settingsBasedTextStyle), InstructionList(recipe, settingsBasedTextStyle) - ])) + ], + ), + ) ], ), ), @@ -333,22 +339,24 @@ class RecipeScreenState extends State { } Widget _showTimers(Recipe recipe) { - List l = TimerList().get(recipe.id); - if (l.length > 0) { + final List l = TimerList().get(recipe.id); + if (l.isNotEmpty) { return Padding( padding: const EdgeInsets.only(bottom: 10.0), - child: Column(children: [ - ListView.builder( - shrinkWrap: true, - itemCount: l.length, - itemBuilder: (context, index) { - return _buildTimerListItem(l[index]); - }, - ) - ]), + child: Column( + children: [ + ListView.builder( + shrinkWrap: true, + itemCount: l.length, + itemBuilder: (context, index) { + return _buildTimerListItem(l[index]); + }, + ) + ], + ), ); } - return SizedBox.shrink(); + return const SizedBox.shrink(); } ListTile _buildTimerListItem(Timer timer) { @@ -358,11 +366,12 @@ class RecipeScreenState extends State { timer: timer, ), trailing: IconButton( - icon: Icon(Icons.cancel), - onPressed: () { - timer.cancel(); - setState(() {}); - }), + icon: const Icon(Icons.cancel), + onPressed: () { + timer.cancel(); + setState(() {}); + }, + ), ); } } diff --git a/lib/src/screens/recipe/widget/ingredient_list.dart b/lib/src/screens/recipe/widget/ingredient_list.dart index d7f4f9fd..f5542edc 100644 --- a/lib/src/screens/recipe/widget/ingredient_list.dart +++ b/lib/src/screens/recipe/widget/ingredient_list.dart @@ -7,14 +7,18 @@ class IngredientList extends StatefulWidget { final Recipe _recipe; final TextStyle _textStyle; - const IngredientList(this._recipe, this._textStyle); + const IngredientList( + this._recipe, + this._textStyle, { + super.key, + }); @override _IngredientListState createState() => _IngredientListState(); } class _IngredientListState extends State { - List _ingredientsDone; + late List _ingredientsDone; @override void initState() { @@ -37,53 +41,54 @@ class _IngredientListState extends State { padding: const EdgeInsets.symmetric(vertical: 8.0), child: ListView.separated( shrinkWrap: true, - physics: ClampingScrollPhysics(), + physics: const ClampingScrollPhysics(), itemBuilder: (context, index) { return widget._recipe.recipeIngredient[index].startsWith('##') - ? Text( - widget._recipe.recipeIngredient[index].replaceFirst( - RegExp(r'##\s*'), - '', - ), - style: widget._textStyle.copyWith( - fontFeatures:[FontFeature.enable('smcp')], - ), - ) - : Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: widget._textStyle.fontSize * 1.5, - height: widget._textStyle.fontSize, - alignment: Alignment.center, - child: _ingredientsDone[index] - ? Icon( - Icons.check_circle, - size: widget._textStyle.fontSize, - color: Colors.green, - ) - : Icon( - Icons.circle, - size: widget._textStyle.fontSize * 0.5, - ), + ? Text( + widget._recipe.recipeIngredient[index].replaceFirst( + RegExp(r'##\s*'), + '', + ), + style: widget._textStyle.copyWith( + fontFeatures: [const FontFeature.enable('smcp')], ), - Expanded( - child: GestureDetector( - onTap: () { - setState(() { - _ingredientsDone[index] = !_ingredientsDone[index]; - }); - }, - child: Text( - widget._recipe.recipeIngredient[index], - style: widget._textStyle, + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: widget._textStyle.fontSize! * 1.5, + height: widget._textStyle.fontSize, + alignment: Alignment.center, + child: _ingredientsDone[index] + ? Icon( + Icons.check_circle, + size: widget._textStyle.fontSize, + color: Colors.green, + ) + : Icon( + Icons.circle, + size: widget._textStyle.fontSize! * 0.5, + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _ingredientsDone[index] = + !_ingredientsDone[index]; + }); + }, + child: Text( + widget._recipe.recipeIngredient[index], + style: widget._textStyle, + ), ), ), - ), - ], - ); + ], + ); }, - separatorBuilder: (c, i) => SizedBox(height: 5), + separatorBuilder: (c, i) => const SizedBox(height: 5), itemCount: widget._recipe.recipeIngredient.length, ), ), diff --git a/lib/src/screens/recipe/widget/instruction_list.dart b/lib/src/screens/recipe/widget/instruction_list.dart index cd6a2b0c..b6d3cd12 100644 --- a/lib/src/screens/recipe/widget/instruction_list.dart +++ b/lib/src/screens/recipe/widget/instruction_list.dart @@ -6,14 +6,18 @@ class InstructionList extends StatefulWidget { final Recipe _recipe; final TextStyle _textStyle; - const InstructionList(this._recipe, this._textStyle); + const InstructionList( + this._recipe, + this._textStyle, { + super.key, + }); @override _InstructionListState createState() => _InstructionListState(); } class _InstructionListState extends State { - List _instructionsDone; + late List _instructionsDone; @override void initState() { @@ -36,7 +40,7 @@ class _InstructionListState extends State { padding: const EdgeInsets.symmetric(vertical: 8.0), child: ListView.separated( shrinkWrap: true, - physics: ClampingScrollPhysics(), + physics: const ClampingScrollPhysics(), itemBuilder: (context, index) { return GestureDetector( onTap: () { @@ -50,17 +54,18 @@ class _InstructionListState extends State { Container( width: 40, height: 40, - margin: EdgeInsets.only(right: 15, top: 10), - child: _instructionsDone[index] - ? Icon(Icons.check) - : Center(child: Text("${index + 1}")), + margin: const EdgeInsets.only(right: 15, top: 10), decoration: ShapeDecoration( - shape: CircleBorder( - side: BorderSide(color: Colors.grey)), + shape: const CircleBorder( + side: BorderSide(color: Colors.grey), + ), color: _instructionsDone[index] ? Colors.green : Theme.of(context).backgroundColor, ), + child: _instructionsDone[index] + ? const Icon(Icons.check) + : Center(child: Text("${index + 1}")), ), Expanded( child: Text( @@ -72,7 +77,7 @@ class _InstructionListState extends State { ), ); }, - separatorBuilder: (c, i) => SizedBox(height: 10), + separatorBuilder: (c, i) => const SizedBox(height: 10), itemCount: widget._recipe.recipeInstructions.length, ), ), diff --git a/lib/src/screens/recipe/widget/nutrition_list.dart b/lib/src/screens/recipe/widget/nutrition_list.dart index d2fcbfae..13bfb0c0 100644 --- a/lib/src/screens/recipe/widget/nutrition_list.dart +++ b/lib/src/screens/recipe/widget/nutrition_list.dart @@ -5,7 +5,10 @@ import 'package:nextcloud_cookbook_flutter/src/screens/recipe/widget/nutrition_l class NutritionList extends StatelessWidget { final Map _nutrition; - const NutritionList(this._nutrition); + const NutritionList( + this._nutrition, { + super.key, + }); @override Widget build(BuildContext context) { @@ -15,16 +18,19 @@ class NutritionList extends StatelessWidget { data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( title: Text(translate('recipe.fields.nutrition.title')), - initiallyExpanded: false, children: [ Wrap( - spacing: 10, - runSpacing: 10, - children: _nutrition.entries - .map((e) => NutritionListItem( - translate('recipe.fields.nutrition.items.' + e.key), - e.value)) - .toList()), + spacing: 10, + runSpacing: 10, + children: _nutrition.entries + .map( + (e) => NutritionListItem( + translate('recipe.fields.nutrition.items.${e.key}'), + e.value, + ), + ) + .toList(), + ), ], ), ), diff --git a/lib/src/screens/recipe/widget/nutrition_list_item.dart b/lib/src/screens/recipe/widget/nutrition_list_item.dart index 173ff5da..974817ca 100644 --- a/lib/src/screens/recipe/widget/nutrition_list_item.dart +++ b/lib/src/screens/recipe/widget/nutrition_list_item.dart @@ -3,7 +3,11 @@ import 'package:flutter/material.dart'; class NutritionListItem extends StatelessWidget { final String name; final String value; - const NutritionListItem(this.name, this.value); + const NutritionListItem( + this.name, + this.value, { + super.key, + }); @override Widget build(BuildContext context) { @@ -11,19 +15,10 @@ class NutritionListItem extends StatelessWidget { child: Column( children: [ Container( - child: Center( - child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: Text( - name, - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), height: 30, decoration: BoxDecoration( color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.only( + borderRadius: const BorderRadius.only( topLeft: Radius.circular(3), topRight: Radius.circular(3), ), @@ -31,19 +26,20 @@ class NutritionListItem extends StatelessWidget { color: Theme.of(context).hintColor, ), ), - ), - Container( child: Center( child: Padding( padding: const EdgeInsets.only(left: 10, right: 10), child: Text( - value, + name, + style: const TextStyle(fontWeight: FontWeight.bold), ), ), ), + ), + Container( height: 30, decoration: BoxDecoration( - borderRadius: BorderRadius.only( + borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(3), bottomRight: Radius.circular(3), ), @@ -51,6 +47,14 @@ class NutritionListItem extends StatelessWidget { color: Theme.of(context).hintColor, ), ), + child: Center( + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Text( + value, + ), + ), + ), ), ], ), diff --git a/lib/src/screens/recipe_create_screen.dart b/lib/src/screens/recipe_create_screen.dart index 55085970..68141447 100644 --- a/lib/src/screens/recipe_create_screen.dart +++ b/lib/src/screens/recipe_create_screen.dart @@ -5,12 +5,15 @@ import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/form/recipe_form.dart'; -import 'recipe/recipe_screen.dart'; +import 'package:nextcloud_cookbook_flutter/src/screens/recipe/recipe_screen.dart'; class RecipeCreateScreen extends StatelessWidget { final Recipe recipe; - const RecipeCreateScreen(this.recipe); + const RecipeCreateScreen( + this.recipe, { + super.key, + }); @override Widget build(BuildContext context) { @@ -19,27 +22,31 @@ class RecipeCreateScreen extends StatelessWidget { child: Scaffold( appBar: AppBar( title: BlocListener( - listener: (BuildContext context, RecipeState state) { - if (state is RecipeCreateFailure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(translate( - 'recipe_create.errors.update_failed', - args: {"error_msg": state.errorMsg})), - backgroundColor: Colors.red, + listener: (BuildContext context, RecipeState state) { + if (state is RecipeCreateFailure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + translate( + 'recipe_create.errors.update_failed', + args: {"error_msg": state.errorMsg}, + ), ), - ); - } else if (state is RecipeCreateSuccess) { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => - RecipeScreen(recipeId: state.recipeId), - ), - ); - } - }, - child: Text(translate('recipe_create.title'))), + backgroundColor: Colors.red, + ), + ); + } else if (state is RecipeCreateSuccess) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => + RecipeScreen(recipeId: state.recipeId), + ), + ); + } + }, + child: Text(translate('recipe_create.title')), + ), ), body: RecipeForm( recipe, diff --git a/lib/src/screens/recipe_edit_screen.dart b/lib/src/screens/recipe_edit_screen.dart index e87186f6..484222b2 100644 --- a/lib/src/screens/recipe_edit_screen.dart +++ b/lib/src/screens/recipe_edit_screen.dart @@ -8,38 +8,45 @@ import 'package:nextcloud_cookbook_flutter/src/screens/form/recipe_form.dart'; class RecipeEditScreen extends StatelessWidget { final Recipe recipe; - const RecipeEditScreen(this.recipe); + const RecipeEditScreen( + this.recipe, { + super.key, + }); @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () { - RecipeBloc recipeBloc = BlocProvider.of(context); + final RecipeBloc recipeBloc = BlocProvider.of(context); if (recipeBloc.state is RecipeUpdateFailure) { - recipeBloc.add(RecipeLoaded(recipeId: recipe.id)); + recipeBloc.add(RecipeLoaded(recipe.id)); } return Future(() => true); }, child: Scaffold( appBar: AppBar( title: BlocListener( - listener: (BuildContext context, RecipeState state) { - if (state is RecipeUpdateFailure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(translate( - 'recipe_edit.errors.update_failed', - args: {"error_msg": state.errorMsg})), - backgroundColor: Colors.red, + listener: (BuildContext context, RecipeState state) { + if (state is RecipeUpdateFailure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + translate( + 'recipe_edit.errors.update_failed', + args: {"error_msg": state.errorMsg}, + ), ), - ); - } else if (state is RecipeUpdateSuccess) { - BlocProvider.of(context) - .add(RecipeLoaded(recipeId: state.recipeId)); - Navigator.pop(context); - } - }, - child: Text(translate('recipe_edit.title'))), + backgroundColor: Colors.red, + ), + ); + } else if (state is RecipeUpdateSuccess) { + BlocProvider.of(context) + .add(RecipeLoaded(state.recipeId)); + Navigator.pop(context); + } + }, + child: Text(translate('recipe_edit.title')), + ), ), body: RecipeForm( recipe, diff --git a/lib/src/screens/recipe_import_screen.dart b/lib/src/screens/recipe_import_screen.dart index e4248b90..2a027f73 100644 --- a/lib/src/screens/recipe_import_screen.dart +++ b/lib/src/screens/recipe_import_screen.dart @@ -8,7 +8,7 @@ import 'package:nextcloud_cookbook_flutter/src/screens/recipe/recipe_screen.dart class RecipeImportScreen extends StatelessWidget { final String importUrl; - RecipeImportScreen([this.importUrl = '']); + const RecipeImportScreen([this.importUrl = '']); @override Widget build(BuildContext context) { @@ -22,18 +22,23 @@ class RecipeImportScreen extends StatelessWidget { if (state is RecipeImportFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(translate( + content: Text( + translate( 'recipe_import.errors.import_failed', - args: {"error_msg": state.errorMsg})), + args: {"error_msg": state.errorMsg}, + ), + ), backgroundColor: Colors.red, ), ); } else if (state is RecipeImportSuccess) { Navigator.push( context, - MaterialPageRoute(builder: (context) { - return RecipeScreen(recipeId: state.recipeId); - }), + MaterialPageRoute( + builder: (context) { + return RecipeScreen(recipeId: state.recipeId); + }, + ), ); } }, diff --git a/lib/src/screens/recipes_list_screen.dart b/lib/src/screens/recipes_list_screen.dart index 5d642285..edaf6556 100644 --- a/lib/src/screens/recipes_list_screen.dart +++ b/lib/src/screens/recipes_list_screen.dart @@ -10,34 +10,36 @@ import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_netw class RecipesListScreen extends StatefulWidget { final String category; - const RecipesListScreen({Key key, @required this.category}) : super(key: key); + const RecipesListScreen({ + super.key, + required this.category, + }); @override State createState() => RecipesListScreenState(); } class RecipesListScreenState extends State { - String category; - @override void initState() { - category = widget.category; super.initState(); } @override Widget build(BuildContext context) { BlocProvider.of(context) - .add(RecipesShortLoaded(category: category)); + .add(RecipesShortLoaded(category: widget.category)); return BlocBuilder( builder: (context, recipesShortState) { return Scaffold( appBar: AppBar( - title: Text(translate( - 'recipe_list.title_category', - args: {'category': category}, - )), + title: Text( + translate( + 'recipe_list.title_category', + args: {'category': widget.category}, + ), + ), actions: [ // action button IconButton( @@ -48,7 +50,7 @@ class RecipesListScreenState extends State { onPressed: () { DefaultCacheManager().emptyCache(); BlocProvider.of(context) - .add(RecipesShortLoaded(category: category)); + .add(RecipesShortLoaded(category: widget.category)); }, ), ], @@ -57,16 +59,16 @@ class RecipesListScreenState extends State { onRefresh: () { DefaultCacheManager().emptyCache(); BlocProvider.of(context) - .add(RecipesShortLoaded(category: category)); - return Future.value(true); + .add(RecipesShortLoaded(category: widget.category)); + return Future.value(); }, - child: (() { + child: () { if (recipesShortState is RecipesShortLoadSuccess) { return _buildRecipesShortScreen(recipesShortState.recipesShort); } else { - return Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator()); } - }()), + }(), ), ); }, @@ -81,7 +83,7 @@ class RecipesListScreenState extends State { itemBuilder: (context, index) { return _buildRecipeShortScreen(data[index]); }, - separatorBuilder: (context, index) => Divider( + separatorBuilder: (context, index) => const Divider( color: Colors.black, ), ), @@ -91,21 +93,19 @@ class RecipesListScreenState extends State { ListTile _buildRecipeShortScreen(RecipeShort recipeShort) { return ListTile( title: Text(recipeShort.name), - trailing: Container( - child: AuthenticationCachedNetworkRecipeImage( - recipeId: recipeShort.recipeId, - full: false, - width: 60, - height: 60, - ), + trailing: AuthenticationCachedNetworkRecipeImage( + recipeId: recipeShort.recipeId, + full: false, + width: 60, + height: 60, ), onTap: () { Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - RecipeScreen(recipeId: recipeShort.recipeId), - )); + context, + MaterialPageRoute( + builder: (context) => RecipeScreen(recipeId: recipeShort.recipeId), + ), + ); }, ); } diff --git a/lib/src/screens/splash_screen.dart b/lib/src/screens/splash_screen.dart index b9b3f729..63f36abb 100644 --- a/lib/src/screens/splash_screen.dart +++ b/lib/src/screens/splash_screen.dart @@ -2,14 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; class SplashPage extends StatelessWidget { + const SplashPage({super.key}); + @override Widget build(BuildContext context) { return Scaffold( body: Center( - child: SpinKitWave( - color: Theme.of(context).primaryColor, - size: 50.0, - )), + child: SpinKitWave( + color: Theme.of(context).primaryColor, + ), + ), ); } } diff --git a/lib/src/screens/timer_screen.dart b/lib/src/screens/timer_screen.dart index 794eda0f..56e8ec5b 100644 --- a/lib/src/screens/timer_screen.dart +++ b/lib/src/screens/timer_screen.dart @@ -6,12 +6,14 @@ import 'package:nextcloud_cookbook_flutter/src/widget/animated_time_progress_bar import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; class TimerScreen extends StatefulWidget { + const TimerScreen({super.key}); + @override _TimerScreen createState() => _TimerScreen(); } class _TimerScreen extends State { - List _list; + late List _list; @override void initState() { @@ -20,7 +22,7 @@ class _TimerScreen extends State { @override Widget build(BuildContext context) { - this._list = TimerList().timers; + _list = TimerList().timers; return Scaffold( appBar: AppBar( @@ -39,9 +41,9 @@ class _TimerScreen extends State { ), ], ), - body: (() { - return _buildTimerScreen(this._list); - }()), + body: () { + return _buildTimerScreen(_list); + }(), ); } @@ -53,7 +55,7 @@ class _TimerScreen extends State { itemBuilder: (context, index) { return _buildListItem(data[index]); }, - separatorBuilder: (context, index) => Divider( + separatorBuilder: (context, index) => const Divider( color: Colors.black, ), ), @@ -69,17 +71,18 @@ class _TimerScreen extends State { width: 60, height: 60, ), - title: Text(timer.title), + title: Text(timer.title!), subtitle: AnimatedTimeProgressBar( timer: timer, ), isThreeLine: true, trailing: IconButton( - icon: Icon(Icons.cancel), - onPressed: () { - timer.cancel(); - setState(() {}); - }), + icon: const Icon(Icons.cancel), + onPressed: () { + timer.cancel(); + setState(() {}); + }, + ), onTap: () { Navigator.push( context, diff --git a/lib/src/services/authentication_provider.dart b/lib/src/services/authentication_provider.dart index aeee643d..aa774871 100644 --- a/lib/src/services/authentication_provider.dart +++ b/lib/src/services/authentication_provider.dart @@ -11,151 +11,167 @@ import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:xml/xml.dart'; class AuthenticationProvider { - final FlutterSecureStorage _secureStorage = new FlutterSecureStorage(); + final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); final String _appAuthenticationKey = 'appAuthentication'; - AppAuthentication currentAppAuthentication; - dio.CancelToken _cancelToken; + AppAuthentication? currentAppAuthentication; + dio.CancelToken? _cancelToken; Future authenticate({ - @required String serverUrl, - @required String username, - @required String originalBasicAuth, - @required bool isSelfSignedCertificate, + required String serverUrl, + required String username, + required String originalBasicAuth, + required bool isSelfSignedCertificate, }) async { - if (serverUrl.substring(0, 4) != 'http') { - serverUrl = 'https://' + serverUrl; - if (serverUrl.endsWith("/")) { - serverUrl = serverUrl.substring(0, serverUrl.length - 1); + String url = serverUrl; + if (url.substring(0, 4) != 'http') { + url = 'https://$url'; + if (url.endsWith("/")) { + url = url.substring(0, url.length - 1); } } - String urlInitialCall = serverUrl + '/ocs/v2.php/core/getapppassword'; + final String urlInitialCall = '$url/ocs/v2.php/core/getapppassword'; dio.Response response; try { - dio.Dio client = dio.Dio(); + final dio.Dio client = dio.Dio(); if (isSelfSignedCertificate) { (client.httpClientAdapter as DefaultHttpClientAdapter) .onHttpClientCreate = (HttpClient httpClient) { httpClient.badCertificateCallback = (X509Certificate cert, String host, int port) => true; + return null; }; } response = await client.get( urlInitialCall, - options: new dio.Options( + options: dio.Options( headers: { "OCS-APIREQUEST": "true", "User-Agent": "Cookbook App", "authorization": originalBasicAuth }, - validateStatus: (status) => status < 500, + validateStatus: (status) => status! < 500, ), cancelToken: _cancelToken, ); } on dio.DioError catch (e) { if (e.message.contains("SocketException")) { - throw (translate("login.errors.not_reachable", - args: {"server_url": serverUrl, "error_msg": e})); + throw translate( + "login.errors.not_reachable", + args: {"server_url": url, "error_msg": e}, + ); } else if (e.message.contains("CERTIFICATE_VERIFY_FAILED")) { - throw (translate("login.errors.certificate_failed", - args: {"server_url": serverUrl, "error_msg": e})); + throw translate( + "login.errors.certificate_failed", + args: {"server_url": url, "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; if (response.statusCode == 200) { String appPassword; try { - appPassword = XmlDocument.parse(response.data) + appPassword = XmlDocument.parse(response.data as String) .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}); } - String basicAuth = - 'Basic ' + base64Encode(utf8.encode('$username:$appPassword')); + final String basicAuth = + 'Basic ${base64Encode(utf8.encode('$username:$appPassword'))}'; return AppAuthentication( - server: serverUrl, + server: url, loginName: username, basicAuth: basicAuth, 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", args: { - "status_code": response.statusCode, - "status_message": response.statusMessage, - })); + throw translate( + "login.errors.failure", + args: { + "status_code": response.statusCode, + "status_message": response.statusMessage, + }, + ); } } Future authenticateAppPassword({ - @required String serverUrl, - @required String username, - @required String basicAuth, - @required bool isSelfSignedCertificate, + required String serverUrl, + required String username, + required String basicAuth, + required bool isSelfSignedCertificate, }) async { - if (serverUrl.substring(0, 4) != 'http') { - serverUrl = 'https://' + serverUrl; + String url = serverUrl; + if (url.substring(0, 4) != 'http') { + url = 'https://$url'; } bool authenticated; try { authenticated = await checkAppAuthentication( - serverUrl, basicAuth, isSelfSignedCertificate); + url, + basicAuth, + isSelfSignedCertificate: isSelfSignedCertificate, + ); } on dio.DioError catch (e) { if (e.message.contains("SocketException")) { - throw (translate("login.errors.not_reachable", - args: {"server_url": serverUrl, "error_msg": e})); + throw translate( + "login.errors.not_reachable", + args: {"server_url": url, "error_msg": e}, + ); } else if (e.message.contains("CERTIFICATE_VERIFY_FAILED")) { - throw (translate("login.errors.certificate_failed", - args: {"server_url": serverUrl, "error_msg": e})); + throw translate( + "login.errors.certificate_failed", + args: {"server_url": url, "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) { return AppAuthentication( - server: serverUrl, + server: url, loginName: username, basicAuth: basicAuth, isSelfSignedCertificate: isSelfSignedCertificate, ); } else { - throw (translate("login.errors.auth_failed")); + throw translate("login.errors.auth_failed"); } } void stopAuthenticate() { - if (_cancelToken != null) { - _cancelToken.cancel("Stopped by the User!"); - _cancelToken = null; - } + _cancelToken?.cancel("Stopped by the User!"); + _cancelToken = null; } Future hasAppAuthentication() async { if (currentAppAuthentication != null) { return true; } else { - String appAuthentication = + final String? appAuthentication = await _secureStorage.read(key: _appAuthenticationKey); return appAuthentication != null; } } Future loadAppAuthentication() async { - String appAuthenticationString = + final String? appAuthenticationString = await _secureStorage.read(key: _appAuthenticationKey); if (appAuthenticationString == null) { - throw (translate('login.errors.authentication_not_found')); + throw translate('login.errors.authentication_not_found'); } else { currentAppAuthentication = AppAuthentication.fromJson(appAuthenticationString); @@ -165,33 +181,37 @@ class AuthenticationProvider { /// If server response is 401 Unauthorized the AppPassword is (no longer?) valid! Future checkAppAuthentication( String serverUrl, - String basicAuth, - bool isSelfSignedCertificate, - ) async { - String urlAuthCheck = - serverUrl + '/index.php/apps/cookbook/api/v1/categories'; + String basicAuth, { + required bool isSelfSignedCertificate, + }) async { + final String urlAuthCheck = + '$serverUrl/index.php/apps/cookbook/api/v1/categories'; dio.Response response; try { - dio.Dio client = dio.Dio(); + final dio.Dio client = dio.Dio(); if (isSelfSignedCertificate) { (client.httpClientAdapter as DefaultHttpClientAdapter) .onHttpClientCreate = (HttpClient httpClient) { httpClient.badCertificateCallback = (X509Certificate cert, String host, int port) => true; + return null; }; } response = await client.get( urlAuthCheck, - options: new dio.Options( + options: dio.Options( headers: {"authorization": basicAuth}, - validateStatus: (status) => status < 500, + validateStatus: (status) => status! < 500, ), ); } on dio.DioError catch (e) { - throw (translate("login.errors.no_internet", args: { - "error_msg": e.message, - })); + throw translate( + "login.errors.no_internet", + args: { + "error_msg": e.message, + }, + ); } if (response.statusCode == 401) { @@ -199,25 +219,31 @@ class AuthenticationProvider { } else if (response.statusCode == 200) { return true; } else { - throw (translate("login.errors.wrong_status", args: { - "error_msg": response.statusCode, - })); + throw translate( + "login.errors.wrong_status", + args: { + "error_msg": response.statusCode, + }, + ); } } Future persistAppAuthentication( - AppAuthentication appAuthentication) async { + AppAuthentication appAuthentication, + ) async { currentAppAuthentication = appAuthentication; await _secureStorage.write( - key: _appAuthenticationKey, value: appAuthentication.toJson()); + key: _appAuthenticationKey, + value: appAuthentication.toJson(), + ); } Future deleteAppAuthentication() async { - var response; + dio.Response? response; try { - response = await currentAppAuthentication.authenticatedClient.delete( - "${currentAppAuthentication.server}/ocs/v2.php/core/apppassword", - options: new dio.Options( + response = await currentAppAuthentication?.authenticatedClient.delete( + "${currentAppAuthentication!.server}/ocs/v2.php/core/apppassword", + options: dio.Options( headers: { "OCS-APIREQUEST": "true", }, diff --git a/lib/src/services/categories_provider.dart b/lib/src/services/categories_provider.dart index 21e46a41..55d3ea34 100644 --- a/lib/src/services/categories_provider.dart +++ b/lib/src/services/categories_provider.dart @@ -1,29 +1,31 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/network.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; -import 'network.dart'; - class CategoriesProvider { Future> fetchCategories() async { - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; final String url = "${appAuthentication.server}/index.php/apps/cookbook/api/v1/categories"; // Parse categories try { - String contents = await Network().get(url); - List categories = Category.parseCategories(contents); + final String contents = await Network().get(url); + final List categories = Category.parseCategories(contents); categories.sort((a, b) => a.name.compareTo(b.name)); categories.insert( 0, Category( translate('categories.all_categories'), - categories.fold(0, - (previousValue, element) => previousValue + element.recipeCount), + categories.fold( + 0, + (int previousValue, Category element) => + previousValue + element.recipeCount, + ), ), ); return categories; diff --git a/lib/src/services/category_recipes_short_provider.dart b/lib/src/services/category_recipes_short_provider.dart index 499b01b6..d0ef5a7e 100644 --- a/lib/src/services/category_recipes_short_provider.dart +++ b/lib/src/services/category_recipes_short_provider.dart @@ -1,30 +1,30 @@ import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/network.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; import 'package:nextcloud_cookbook_flutter/src/services/version_provider.dart'; -import 'network.dart'; - class CategoryRecipesShortProvider { Future> fetchCategoryRecipesShort(String category) async { - AndroidApiVersion androidApiVersion = UserRepository().getAndroidVersion(); + final AndroidApiVersion androidApiVersion = + UserRepository().getAndroidVersion(); - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; String url = "${appAuthentication.server}/apps/cookbook/api/v1/category/$category"; - if (androidApiVersion != AndroidApiVersion.BEFORE_API_ENDPOINT) { - category = category == "*" + if (androidApiVersion != AndroidApiVersion.beforeApiEndpoint) { + final categorySanitized = category == "*" ? "_" : category; // Mapping from * to _ for recipes without a category! url = - "${appAuthentication.server}/index.php/apps/cookbook/api/v1/category/$category"; + "${appAuthentication.server}/index.php/apps/cookbook/api/v1/category/$categorySanitized"; } // Parse categories try { - String contents = await Network().get(url); + final String contents = await Network().get(url); return RecipeShort.parseRecipesShort(contents); } catch (e) { throw Exception(e); diff --git a/lib/src/services/category_search_provider.dart b/lib/src/services/category_search_provider.dart index 49692ae5..8b4b2de6 100644 --- a/lib/src/services/category_search_provider.dart +++ b/lib/src/services/category_search_provider.dart @@ -7,7 +7,10 @@ class CategorySearchProvider { static String categoryAll = translate('categories.all_categories'); void updateCategoryNames(List categories) { - categoryNames = categories.map((e) => e.name).where((element) => element != categoryAll && element != '*').toList(); + categoryNames = categories + .map((e) => e.name) + .where((element) => element != categoryAll && element != '*') + .toList(); categoriesLoaded = true; } diff --git a/lib/src/services/data_repository.dart b/lib/src/services/data_repository.dart index bae73e0d..5470042a 100644 --- a/lib/src/services/data_repository.dart +++ b/lib/src/services/data_repository.dart @@ -13,11 +13,10 @@ import 'package:nextcloud_cookbook_flutter/src/services/recipes_short_provider.d class DataRepository { // Singleton - static final DataRepository _dataRepository = DataRepository._internal(); - factory DataRepository() { - return _dataRepository; - } - DataRepository._internal(); + static final DataRepository _dataRepository = DataRepository._(); + factory DataRepository() => _dataRepository; + + DataRepository._(); // Provider List RecipesShortProvider recipesShortProvider = RecipesShortProvider(); @@ -26,14 +25,13 @@ class DataRepository { CategorySearchProvider categorySearchProvider = CategorySearchProvider(); RecipeProvider recipeProvider = RecipeProvider(); CategoriesProvider categoriesProvider = CategoriesProvider(); - NextcloudMetadataApi _nextcloudMetadataApi = NextcloudMetadataApi(); + final NextcloudMetadataApi _nextcloudMetadataApi = NextcloudMetadataApi(); // Data - static Future> _allRecipesShort; static String categoryAll = translate('categories.all_categories'); // Actions - Future> fetchRecipesShort({String category}) { + Future> fetchRecipesShort({required String category}) { if (category == categoryAll) { return recipesShortProvider.fetchRecipesShort(); } else { @@ -41,16 +39,19 @@ class DataRepository { } } - Future fetchRecipe(int id) { - return recipeProvider.fetchRecipe(id); + Future fetchRecipe(String id) { + return recipeProvider.fetchRecipe(int.parse(id)); } - Future updateRecipe(Recipe recipe) { - return recipeProvider.updateRecipe(recipe); + Future updateRecipe(Recipe recipe) async { + final response = await recipeProvider.updateRecipe(recipe); + return response.toString(); } - Future createRecipe(Recipe recipe) { - return recipeProvider.createRecipe(recipe); + Future createRecipe(Recipe recipe) async { + final response = await recipeProvider.createRecipe(recipe); + + return response.toString(); } Future importRecipe(String url) { @@ -62,8 +63,9 @@ class DataRepository { } Future> fetchCategoryMainRecipes( - List categories) async { - return await Future.wait( + List categories, + ) async { + return Future.wait( categories.map((category) => _fetchCategoryMainRecipe(category)).toList(), ); } @@ -72,29 +74,25 @@ class DataRepository { List categoryRecipes = []; try { - categoryRecipes = await () { - if (category.name == translate('categories.all_categories')) { - return recipesShortProvider.fetchRecipesShort(); - } else { - return categoryRecipesShortProvider - .fetchCategoryRecipesShort(category.name); - } - }(); + if (category.name == translate('categories.all_categories')) { + categoryRecipes = await recipesShortProvider.fetchRecipesShort(); + } else { + categoryRecipes = await categoryRecipesShortProvider + .fetchCategoryRecipesShort(category.name); + } } catch (e) { log("Could not load main recipe of Category!"); } - if (categoryRecipes.length > 0) { - category.firstRecipeId = categoryRecipes.first.recipeId; - } else { - category.firstRecipeId = 0; + if (categoryRecipes.isNotEmpty) { + return category.copyWith(firstRecipeId: categoryRecipes.first.recipeId); } return category; } Future> fetchAllRecipes() async { - return await fetchRecipesShort(category: "All"); + return fetchRecipesShort(category: categoryAll); } String getUserAvatarUrl() { @@ -106,8 +104,9 @@ class DataRepository { } Future> getMatchingCategoryNames(String pattern) async { - if (!categorySearchProvider.categoriesLoaded) + if (!categorySearchProvider.categoriesLoaded) { await categoriesProvider.fetchCategories(); + } return categorySearchProvider.getMatchingCategoryNames(pattern); } diff --git a/lib/src/services/intent_repository.dart b/lib/src/services/intent_repository.dart index d719ccdd..5f4e7a97 100644 --- a/lib/src/services/intent_repository.dart +++ b/lib/src/services/intent_repository.dart @@ -1,29 +1,28 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe_import_screen.dart'; class IntentRepository { // Singleton Pattern - static final IntentRepository _intentRepository = - IntentRepository._internal(); - factory IntentRepository() { - return _intentRepository; - } - IntentRepository._internal(); + static final IntentRepository _intentRepository = IntentRepository._(); + factory IntentRepository() => _intentRepository; + + IntentRepository._(); - static final _navigationKey = new GlobalKey(); + static final _navigationKey = GlobalKey(); static const platform = MethodChannel('app.channel.shared.data'); - void handleIntent() async { - var importUrl = await platform.invokeMethod('getImportUrl'); + Future handleIntent() async { + final importUrl = await platform.invokeMethod('getImportUrl') as String?; if (importUrl != null) { - _navigationKey.currentState.pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) => () { - return RecipeImportScreen(importUrl); - }()), - ModalRoute.withName('/')); + _navigationKey.currentState?.pushAndRemoveUntil( + MaterialPageRoute( + builder: (BuildContext context) => () { + return RecipeImportScreen(importUrl); + }(), + ), + ModalRoute.withName('/'), + ); } } diff --git a/lib/src/services/net/nextcloud_metadata_api.dart b/lib/src/services/net/nextcloud_metadata_api.dart index ac9f620d..5a15df75 100644 --- a/lib/src/services/net/nextcloud_metadata_api.dart +++ b/lib/src/services/net/nextcloud_metadata_api.dart @@ -4,13 +4,14 @@ import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; class NextcloudMetadataApi { final AppAuthentication _appAuthentication; - NextcloudMetadataApi._(this._appAuthentication); - factory NextcloudMetadataApi() { - return new NextcloudMetadataApi._( - UserRepository().getCurrentAppAuthentication()); + return NextcloudMetadataApi._( + UserRepository().currentAppAuthentication, + ); } + NextcloudMetadataApi._(this._appAuthentication); + String getUserAvatarUrl() { return "${_appAuthentication.server}/avatar/${_appAuthentication.loginName}/80"; } diff --git a/lib/src/services/network.dart b/lib/src/services/network.dart index 3d0d26ad..d70b9a37 100644 --- a/lib/src/services/network.dart +++ b/lib/src/services/network.dart @@ -4,31 +4,27 @@ import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; import 'package:nextcloud_cookbook_flutter/src/util/custom_cache_manager.dart'; class Network { - static final Network _network = Network._internal(); + static final Network _network = Network._(); - factory Network() { - return _network; - } + factory Network() => _network; - Network._internal(); + Network._(); /// Try to load file from locale cache first, if not available get it from the server Future get(String url) async { - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; + + final FileInfo file = await DefaultCacheManager().getFileFromCache(url) ?? + // Download, if not available + await CustomCacheManager().getInstance().downloadFile( + url, + authHeaders: { + "Authorization": appAuthentication.basicAuth, + }, + ); - FileInfo file = await DefaultCacheManager().getFileFromCache(url); - if (file == null) { - // Download, if not available - file = await CustomCacheManager.getInstance() - .downloadFile(url, authHeaders: { - "Authorization": appAuthentication.basicAuth, - }); - if (file == null) { - throw Exception("could not download " + url); - } - } - String contents = await file.file.readAsString(); + final String contents = await file.file.readAsString(); return contents; } } diff --git a/lib/src/services/notification_provider.dart b/lib/src/services/notification_provider.dart index 0d33b913..c6f4f9a6 100644 --- a/lib/src/services/notification_provider.dart +++ b/lib/src/services/notification_provider.dart @@ -1,14 +1,11 @@ import 'dart:convert'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; import 'package:timezone/data/latest_10y.dart' as tz; import 'package:timezone/timezone.dart' as tz; -import '../models/timer.dart'; - const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails( '1', @@ -22,37 +19,40 @@ const AndroidNotificationDetails androidPlatformChannelSpecifics = const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); -final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = +final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); class NotificationService { static final NotificationService _notificationService = - NotificationService._internal(); + NotificationService._(); int curId = 0; - factory NotificationService() { - return _notificationService; - } + factory NotificationService() => _notificationService; - NotificationService._internal(); + NotificationService._(); Future init() async { // initialize Timezone Database tz.initializeTimeZones(); tz.setLocalLocation( - tz.getLocation(await FlutterNativeTimezone.getLocalTimezone())); + tz.getLocation(await FlutterNativeTimezone.getLocalTimezone()), + ); - final AndroidInitializationSettings initializationSettingsAndroid = + const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('notification_icon'); Future onDidReceiveLocalNotification( - int id, String title, String body, String payload) async { + int id, + String? title, + String? body, + String? payload, + ) async { // display a dialog with the notification details, tap ok to go to another page - showDialog( - //context: context, + /* showDialog( + // context: context, builder: (BuildContext context) => CupertinoAlertDialog( - title: Text(title), - content: Text(body), + title: Text(title!), + content: Text(body!), actions: [ CupertinoDialogAction( isDefaultAction: true, @@ -63,50 +63,64 @@ class NotificationService { ) ], ), - ); + ); */ } - final IOSInitializationSettings initializationSettingsIOS = - IOSInitializationSettings(onDidReceiveLocalNotification: onDidReceiveLocalNotification); + final DarwinInitializationSettings initializationSettingsIOS = + DarwinInitializationSettings( + onDidReceiveLocalNotification: onDidReceiveLocalNotification, + ); final InitializationSettings initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, iOS: initializationSettingsIOS, macOS: null); + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); // Notification was triggered and the user clicked on it - Future selectNotification(String payload) async { + Future selectNotification(NotificationResponse payload) async { // Map data = jsonDecode(payload); // We could e.g. show the recipe } - await flutterLocalNotificationsPlugin.initialize(initializationSettings, - onSelectNotification: selectNotification); + await _localNotifications.initialize( + initializationSettings, + onDidReceiveNotificationResponse: selectNotification, + ); // Loading pending notifications an rebuild timers - final List pendingNotificationRequests = - await flutterLocalNotificationsPlugin.pendingNotificationRequests(); - pendingNotificationRequests.forEach((PendingNotificationRequest element) { - Map data = jsonDecode(element.payload); - Timer timer = Timer.fromJson(data, element.id); - if (timer.id > this.curId) this.curId = timer.id; - }); + final pendingNotificationRequests = + await _localNotifications.pendingNotificationRequests(); + for (final element in pendingNotificationRequests) { + if (element.payload != null) { + final data = jsonDecode(element.payload!) as Map; + final Timer timer = Timer.fromJson(data, element.id); + if (timer.id > curId) curId = timer.id; + } + } } int start(Timer timer) { - this.curId++; - timer.id = this.curId; - flutterLocalNotificationsPlugin.zonedSchedule(this.curId, timer.title, - timer.body, timer.done, platformChannelSpecifics, - payload: jsonEncode(timer.toJson()), - androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.wallClockTime); - return this.curId; + curId++; + timer.id = curId; + _localNotifications.zonedSchedule( + curId, + timer.title, + timer.body, + timer.done, + platformChannelSpecifics, + payload: jsonEncode(timer.toJson()), + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.wallClockTime, + ); + return curId; } - cancel(Timer timer) { - flutterLocalNotificationsPlugin.cancel(timer.id); + void cancel(Timer timer) { + _localNotifications.cancel(timer.id); } - cancelAll() { - flutterLocalNotificationsPlugin.cancelAll(); + void cancelAll() { + _localNotifications.cancelAll(); } } diff --git a/lib/src/services/recipe_provider.dart b/lib/src/services/recipe_provider.dart index 08619ade..ad7ea668 100644 --- a/lib/src/services/recipe_provider.dart +++ b/lib/src/services/recipe_provider.dart @@ -2,20 +2,19 @@ import 'package:dio/dio.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/network.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; -import 'network.dart'; - class RecipeProvider { Future fetchRecipe(int id) async { - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; final String url = "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes/$id"; // Parse categories try { - String contents = await Network().get(url); + final String contents = await Network().get(url); return Recipe(contents); } catch (e) { throw Exception(e); @@ -23,58 +22,62 @@ class RecipeProvider { } Future updateRecipe(Recipe recipe) async { - Dio client = UserRepository().getAuthenticatedClient(); - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final Dio client = UserRepository().authenticatedClient; + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; try { final String url = "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes/${recipe.id}"; - var response = await client.put(url, - data: recipe.toJson(), - options: new Options( - contentType: "application/json;charset=UTF-8", - )); + final response = await client.put( + url, + data: recipe.toJson(), + options: Options( + contentType: "application/json;charset=UTF-8", + ), + ); // Refresh recipe in the cache await DefaultCacheManager().removeFile(url); - return int.parse(response.data); + return int.parse(response.data as String); } catch (e) { throw Exception(e); } } Future createRecipe(Recipe recipe) async { - Dio client = UserRepository().getAuthenticatedClient(); - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final Dio client = UserRepository().authenticatedClient; + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; try { - var response = await client.post( - "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes", - data: recipe.toJson(), - options: new Options( - contentType: "application/json;charset=UTF-8", - )); - return int.parse(response.data); + final response = await client.post( + "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes", + data: recipe.toJson(), + options: Options( + contentType: "application/json;charset=UTF-8", + ), + ); + return int.parse(response.data as String); } catch (e) { throw Exception(e); } } Future importRecipe(String url) async { - Dio client = UserRepository().getAuthenticatedClient(); - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final Dio client = UserRepository().authenticatedClient; + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; try { - var response = await client.post( - "${appAuthentication.server}/index.php/apps/cookbook/api/v1/import", - data: {"url": url}, - options: new Options( - contentType: "application/json;charset=UTF-8", - )); + final response = await client.post( + "${appAuthentication.server}/index.php/apps/cookbook/api/v1/import", + data: {"url": url}, + options: Options( + contentType: "application/json;charset=UTF-8", + ), + ); - return Recipe(response.data); + return Recipe(response.data as String); } on DioError catch (e) { throw Exception(e.response); } catch (e) { diff --git a/lib/src/services/recipes_short_provider.dart b/lib/src/services/recipes_short_provider.dart index 21e33c6b..5d8fa7c7 100644 --- a/lib/src/services/recipes_short_provider.dart +++ b/lib/src/services/recipes_short_provider.dart @@ -1,21 +1,20 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/network.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; -import 'network.dart'; - class RecipesShortProvider { Future> fetchRecipesShort() async { - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; - final String url = "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes"; + final String url = + "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes"; try { - String contents = await Network().get(url); - return RecipeShort.parseRecipesShort(contents); - } - catch (e) { + final String contents = await Network().get(url); + return RecipeShort.parseRecipesShort(contents); + } catch (e) { throw Exception(translate('recipe_list.errors.load_failed')); } } diff --git a/lib/src/services/user_repository.dart b/lib/src/services/user_repository.dart index 28397368..00eaac35 100644 --- a/lib/src/services/user_repository.dart +++ b/lib/src/services/user_repository.dart @@ -1,18 +1,16 @@ import 'dart:async'; import 'package:dio/dio.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/services/authentication_provider.dart'; import 'package:nextcloud_cookbook_flutter/src/services/version_provider.dart'; -import '../models/app_authentication.dart'; - class UserRepository { // Singleton - static final UserRepository _userRepository = UserRepository._internal(); - factory UserRepository() { - return _userRepository; - } - UserRepository._internal(); + static final UserRepository _userRepository = UserRepository._(); + factory UserRepository() => _userRepository; + + UserRepository._(); AuthenticationProvider authenticationProvider = AuthenticationProvider(); VersionProvider versionProvider = VersionProvider(); @@ -20,9 +18,9 @@ class UserRepository { Future authenticate( String serverUrl, String username, - String originalBasicAuth, - bool isSelfSignedCertificate, - ) async { + String originalBasicAuth, { + required bool isSelfSignedCertificate, + }) async { return authenticationProvider.authenticate( serverUrl: serverUrl, username: username, @@ -34,9 +32,9 @@ class UserRepository { Future authenticateAppPassword( String serverUrl, String username, - String basicAuth, - bool isSelfSignedCertificate, - ) async { + String basicAuth, { + required bool isSelfSignedCertificate, + }) async { return authenticationProvider.authenticateAppPassword( serverUrl: serverUrl, username: username, @@ -49,12 +47,12 @@ class UserRepository { authenticationProvider.stopAuthenticate(); } - AppAuthentication getCurrentAppAuthentication() { - return authenticationProvider.currentAppAuthentication; + AppAuthentication get currentAppAuthentication { + return authenticationProvider.currentAppAuthentication!; } - Dio getAuthenticatedClient() { - return authenticationProvider.currentAppAuthentication.authenticatedClient; + Dio get authenticatedClient { + return currentAppAuthentication.authenticatedClient; } Future hasAppAuthentication() async { @@ -67,14 +65,15 @@ class UserRepository { Future checkAppAuthentication() async { return authenticationProvider.checkAppAuthentication( - authenticationProvider.currentAppAuthentication.server, - authenticationProvider.currentAppAuthentication.basicAuth, - authenticationProvider - .currentAppAuthentication.isSelfSignedCertificate); + currentAppAuthentication.server, + currentAppAuthentication.basicAuth, + isSelfSignedCertificate: currentAppAuthentication.isSelfSignedCertificate, + ); } Future persistAppAuthentication( - AppAuthentication appAuthentication) async { + AppAuthentication appAuthentication, + ) async { return authenticationProvider.persistAppAuthentication(appAuthentication); } diff --git a/lib/src/services/version_provider.dart b/lib/src/services/version_provider.dart index e37f77da..d011e298 100644 --- a/lib/src/services/version_provider.dart +++ b/lib/src/services/version_provider.dart @@ -4,22 +4,22 @@ import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; class VersionProvider { - ApiVersion _currentApiVersion; + late ApiVersion _currentApiVersion; bool warningWasShown = false; Future fetchApiVersion() async { warningWasShown = false; - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; - var response = await appAuthentication.authenticatedClient + final response = await appAuthentication.authenticatedClient .get("${appAuthentication.server}/index.php/apps/cookbook/api/version"); if (response.statusCode == 200 && !response.data.toString().startsWith("")) { try { - _currentApiVersion = ApiVersion.decodeJsonApiVersion(response.data); + _currentApiVersion = ApiVersion.fromJson(response.data.toString()); } catch (e) { _currentApiVersion = ApiVersion(0, 0, 0, 0, 0); _currentApiVersion.loadFailureMessage = e.toString(); @@ -37,8 +37,8 @@ class VersionProvider { } class ApiVersion { - static const int CONFIRMED_MAJOR_API_VERSION = 1; - static const int CONFIRMED_MINOR_API_VERSION = 0; + static const int confirmedMajorAPIVersion = 1; + static const int confirmedMinorAPIVersion = 1; final int majorApiVersion; final int minorApiVersion; @@ -56,16 +56,16 @@ class ApiVersion { this.patchAppVersion, ); - static ApiVersion decodeJsonApiVersion(jsonString) { - Map data = json.decode(jsonString); + factory ApiVersion.fromJson(String jsonString) { + final data = json.decode(jsonString) as Map; if (!(data.containsKey("cookbook_version") && data.containsKey("api_version"))) { throw Exception("Required Fields not present!\n$jsonString"); } - List appVersion = data["cookbook_version"].cast(); - var apiVersion = data["api_version"]; + final appVersion = (data["cookbook_version"] as List).cast(); + final apiVersion = data["api_version"] as Map; if (!(appVersion.length == 3 && apiVersion.containsKey("major") && @@ -74,8 +74,8 @@ class ApiVersion { } return ApiVersion( - apiVersion["major"], - apiVersion["minor"], + apiVersion["major"] as int, + apiVersion["minor"] as int, appVersion[0], appVersion[1], appVersion[2], @@ -86,16 +86,16 @@ class ApiVersion { /// Versions only need to be adapted if backwards comparability is required. AndroidApiVersion getAndroidVersion() { if (majorApiVersion == 0 && minorApiVersion == 0) { - return AndroidApiVersion.BEFORE_API_ENDPOINT; + return AndroidApiVersion.beforeApiEndpoint; } else { - return AndroidApiVersion.CATEGORY_API_TRANSITION; + return AndroidApiVersion.categoryApiTransition; } } bool isVersionAboveConfirmed() { - if (majorApiVersion > CONFIRMED_MAJOR_API_VERSION || - (majorApiVersion == CONFIRMED_MAJOR_API_VERSION && - minorApiVersion > CONFIRMED_MINOR_API_VERSION)) { + if (majorApiVersion > confirmedMajorAPIVersion || + (majorApiVersion == confirmedMajorAPIVersion && + minorApiVersion > confirmedMinorAPIVersion)) { return true; } else { return false; @@ -108,4 +108,4 @@ class ApiVersion { } } -enum AndroidApiVersion { BEFORE_API_ENDPOINT, CATEGORY_API_TRANSITION } +enum AndroidApiVersion { beforeApiEndpoint, categoryApiTransition } diff --git a/lib/src/util/custom_cache_manager.dart b/lib/src/util/custom_cache_manager.dart index 9129ce76..71426e59 100644 --- a/lib/src/util/custom_cache_manager.dart +++ b/lib/src/util/custom_cache_manager.dart @@ -5,10 +5,16 @@ import 'package:http/io_client.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; class CustomCacheManager { + static final CustomCacheManager _instance = CustomCacheManager._(); + + factory CustomCacheManager() => _instance; + + CustomCacheManager._(); + static const key = 'customCacheKey'; - static UserRepository userRepository = UserRepository(); + final UserRepository _userRepository = UserRepository(); - static CacheManager selfSignedCacheManager = CacheManager( + final CacheManager _selfSignedCacheManager = CacheManager( Config( key, fileService: HttpFileService( @@ -21,9 +27,9 @@ class CustomCacheManager { ), ); - static CacheManager getInstance() { - if (userRepository.getCurrentAppAuthentication().isSelfSignedCertificate) { - return selfSignedCacheManager; + CacheManager getInstance() { + if (_userRepository.currentAppAuthentication.isSelfSignedCertificate) { + return _selfSignedCacheManager; } else { return DefaultCacheManager(); } diff --git a/lib/src/util/iso_time_format.dart b/lib/src/util/iso_time_format.dart index c3b6b1d6..343e15a4 100644 --- a/lib/src/util/iso_time_format.dart +++ b/lib/src/util/iso_time_format.dart @@ -1,10 +1,12 @@ +// ignore_for_file: avoid_classes_with_only_static_members + class IsoTimeFormat { // https://dev.to/ashishrawat2911/parse-iso8601-duration-string-to-duration-object-in-dart-flutter-1gc1 static Duration toDuration(String isoString) { if (!RegExp( - r"^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$") - .hasMatch(isoString)) { + r"^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$", + ).hasMatch(isoString)) { throw ArgumentError("String does not follow correct format"); } @@ -26,10 +28,12 @@ class IsoTimeFormat { static int _parseTime(String duration, String timeUnit) { final timeMatch = RegExp(r"\d+" + timeUnit).firstMatch(duration); - if (timeMatch == null) { + final timeString = timeMatch?.group(0); + + if (timeString == null) { return 0; } - final timeString = timeMatch.group(0); + return int.parse(timeString.substring(0, timeString.length - 1)); } } diff --git a/lib/src/util/lifecycle_event_handler.dart b/lib/src/util/lifecycle_event_handler.dart index 8e51d825..b6cf8666 100644 --- a/lib/src/util/lifecycle_event_handler.dart +++ b/lib/src/util/lifecycle_event_handler.dart @@ -3,10 +3,10 @@ import 'package:flutter/foundation.dart'; class LifecycleEventHandler extends WidgetsBindingObserver { final AsyncCallback resumeCallBack; - final AsyncCallback suspendingCallBack; + final AsyncCallback? suspendingCallBack; LifecycleEventHandler({ - this.resumeCallBack, + required this.resumeCallBack, this.suspendingCallBack, }); @@ -14,16 +14,12 @@ class LifecycleEventHandler extends WidgetsBindingObserver { Future didChangeAppLifecycleState(AppLifecycleState state) async { switch (state) { case AppLifecycleState.resumed: - if (resumeCallBack != null) { - await resumeCallBack(); - } + await resumeCallBack(); break; case AppLifecycleState.inactive: case AppLifecycleState.paused: case AppLifecycleState.detached: - if (suspendingCallBack != null) { - await suspendingCallBack(); - } + await suspendingCallBack?.call(); break; } } diff --git a/lib/src/util/nutrition_utilty.dart b/lib/src/util/nutrition_utilty.dart index 82f75b5d..70f0e2ea 100644 --- a/lib/src/util/nutrition_utilty.dart +++ b/lib/src/util/nutrition_utilty.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_classes_with_only_static_members + class NutritionUtility { static final nutritionProperties = [ "calories", diff --git a/lib/src/util/self_signed_certificate_http_overrides.dart b/lib/src/util/self_signed_certificate_http_overrides.dart index 2000d5f9..98704878 100644 --- a/lib/src/util/self_signed_certificate_http_overrides.dart +++ b/lib/src/util/self_signed_certificate_http_overrides.dart @@ -2,7 +2,7 @@ import 'dart:io'; class SelfSignedCertificateHttpOverride extends HttpOverrides { @override - HttpClient createHttpClient(SecurityContext context) { + HttpClient createHttpClient(SecurityContext? context) { return super.createHttpClient(context) ..badCertificateCallback = (X509Certificate cert, String host, int port) => true; diff --git a/lib/src/util/setting_keys.dart b/lib/src/util/setting_keys.dart index 03e1d7c1..12a9789e 100644 --- a/lib/src/util/setting_keys.dart +++ b/lib/src/util/setting_keys.dart @@ -1,3 +1,5 @@ +// ignore_for_file: constant_identifier_names + enum SettingKeys { dark_mode, language, diff --git a/lib/src/util/supported_locales.dart b/lib/src/util/supported_locales.dart index 9fa7e5f9..75ac7480 100644 --- a/lib/src/util/supported_locales.dart +++ b/lib/src/util/supported_locales.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_classes_with_only_static_members + class SupportedLocales { static final locales = { 'bg_BG': 'Български език (Bulgaria)', @@ -5,6 +7,7 @@ class SupportedLocales { 'de_DE': 'Deutsch (Deutschland)', 'de': 'Deutsch', 'en': 'English', + 'en_GB': 'English (United Kingdom)', 'es': 'Español', 'eu': 'euskara', 'fi_FI': 'suomi (Finland)', diff --git a/lib/src/util/theme_mode_manager.dart b/lib/src/util/theme_mode_manager.dart index 74313191..01b13801 100644 --- a/lib/src/util/theme_mode_manager.dart +++ b/lib/src/util/theme_mode_manager.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; @@ -9,8 +8,8 @@ class ThemeModeManager implements IThemeModeManager { Future loadThemeMode() { return Future.value( Settings.getValue( - describeEnum(SettingKeys.dark_mode), - ThemeMode.system.toString(), + SettingKeys.dark_mode.name, + defaultValue: ThemeMode.system.toString(), ), ); } diff --git a/lib/src/util/translate_preferences.dart b/lib/src/util/translate_preferences.dart index aea2146d..863aee48 100644 --- a/lib/src/util/translate_preferences.dart +++ b/lib/src/util/translate_preferences.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'dart:ui'; -import 'package:flutter/foundation.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; @@ -9,14 +8,14 @@ import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; class TranslatePreferences implements ITranslatePreferences { @override Future getPreferredLocale() { - var locale = Settings.getValue( - describeEnum(SettingKeys.language), - Platform.localeName, + final locale = Settings.getValue( + SettingKeys.language.name, + defaultValue: Platform.localeName, ); if (locale == 'default') { return Future.value(Locale(Platform.localeName)); } - return Future.value(Locale(locale)); + return Future.value(Locale(locale!)); } @override diff --git a/lib/src/widget/animated_time_progress_bar.dart b/lib/src/widget/animated_time_progress_bar.dart index 1c8ce976..6e9331c6 100644 --- a/lib/src/widget/animated_time_progress_bar.dart +++ b/lib/src/widget/animated_time_progress_bar.dart @@ -5,75 +5,78 @@ import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; class AnimatedTimeProgressBar extends StatefulWidget { final Timer timer; - const AnimatedTimeProgressBar({@required this.timer, Key key}) - : super(key: key); + const AnimatedTimeProgressBar({ + super.key, + required this.timer, + }); @override _AnimatedTimeProgressBarState createState() => - _AnimatedTimeProgressBarState(timer); + _AnimatedTimeProgressBarState(); } class _AnimatedTimeProgressBarState extends State with TickerProviderStateMixin { - AnimationController _controller; - final Timer _timer; - Tween _timerTween; + late AnimationController _controller; + late Timer _timer; + late Tween _timerTween; - _AnimatedTimeProgressBarState(this._timer) { - this._timerTween = Tween( - begin: this._timer.progress(), - end: 1.0, - ); - } + _AnimatedTimeProgressBarState(); @override void initState() { - super.initState(); + _timer = widget.timer; - this._controller = AnimationController( + _timerTween = Tween( + begin: _timer.progress(), + end: 1.0, + ); + + _controller = AnimationController( duration: _timer.remaining(), vsync: this, ); - this._controller.forward().whenCompleteOrCancel(() { - }); - + _controller.forward().whenCompleteOrCancel(() {}); + super.initState(); } @override void dispose() { - this._controller.dispose(); + _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( - animation: this._controller, + animation: _controller, child: Container(), - builder: (context, child){ - if(_controller.isCompleted){ - return Container(child: Text(translate('timer.done'))); + builder: (context, child) { + if (_controller.isCompleted) { + return Text(translate('timer.done')); } return Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("${_timer.remaining().inHours.toString().padLeft(2, '0')}:${_timer.remaining().inMinutes.remainder(60).toString().padLeft(2, '0')}:${(_timer.remaining().inSeconds.remainder(60)).toString().padLeft(2, '0')}"), - Text("${_timer.duration.inHours.toString().padLeft(2, '0')}:${_timer.duration.inMinutes.remainder(60).toString().padLeft(2, '0')}:${(_timer.duration.inSeconds.remainder(60)).toString().padLeft(2, '0')}"), - ], - ), - + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${_timer.remaining().inHours.toString().padLeft(2, '0')}:${_timer.remaining().inMinutes.remainder(60).toString().padLeft(2, '0')}:${(_timer.remaining().inSeconds.remainder(60)).toString().padLeft(2, '0')}", + ), + Text( + "${_timer.duration.inHours.toString().padLeft(2, '0')}:${_timer.duration.inMinutes.remainder(60).toString().padLeft(2, '0')}:${(_timer.duration.inSeconds.remainder(60)).toString().padLeft(2, '0')}", + ), + ], + ), LinearProgressIndicator( - value: this._timerTween.evaluate(this._controller), + value: _timerTween.evaluate(_controller) as double?, semanticsLabel: _timer.title, ) ], ); - } - + }, ); } } diff --git a/lib/src/widget/api_version_warning.dart b/lib/src/widget/api_version_warning.dart index cfa167e6..68be49c9 100644 --- a/lib/src/widget/api_version_warning.dart +++ b/lib/src/widget/api_version_warning.dart @@ -4,10 +4,12 @@ import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; import 'package:nextcloud_cookbook_flutter/src/services/version_provider.dart'; class ApiVersionWarning extends StatelessWidget { + const ApiVersionWarning({super.key}); + @override Widget build(BuildContext context) { - VersionProvider versionProvider = UserRepository().versionProvider; - ApiVersion apiVersion = versionProvider.getApiVersion(); + final VersionProvider versionProvider = UserRepository().versionProvider; + final ApiVersion apiVersion = versionProvider.getApiVersion(); if (!versionProvider.warningWasShown) { versionProvider.warningWasShown = true; @@ -31,9 +33,8 @@ class ApiVersionWarning extends StatelessWidget { translate( "categories.errors.api_version_above_confirmed", args: { - "version": apiVersion.majorApiVersion.toString() + - "." + - apiVersion.minorApiVersion.toString() + "version": + "${apiVersion.majorApiVersion}.${apiVersion.minorApiVersion}" }, ), ), diff --git a/lib/src/widget/authentication_cached_network_image.dart b/lib/src/widget/authentication_cached_network_image.dart index 2a2cd0e8..05efde33 100644 --- a/lib/src/widget/authentication_cached_network_image.dart +++ b/lib/src/widget/authentication_cached_network_image.dart @@ -4,25 +4,27 @@ import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; class AuthenticationCachedNetworkImage extends StatelessWidget { - final double width; - final double height; - final BoxFit boxFit; + final double? width; + final double? height; + final BoxFit? boxFit; final String url; - final Widget errorWidget; + final Widget? errorWidget; - AuthenticationCachedNetworkImage( - {@required this.url, - this.width, - this.height, - this.boxFit, - this.errorWidget}); + const AuthenticationCachedNetworkImage({ + super.key, + required this.url, + this.width, + this.height, + this.boxFit, + this.errorWidget, + }); @override Widget build(BuildContext context) { - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; return CachedNetworkImage( fit: boxFit, @@ -33,7 +35,9 @@ class AuthenticationCachedNetworkImage extends StatelessWidget { "Accept": "image/jpeg" }, imageUrl: url, - placeholder: (context, url) => Container(child: Center(child: CircularProgressIndicator(),),), + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), errorWidget: (context, url, error) => Container( width: width, height: height, diff --git a/lib/src/widget/authentication_cached_network_recipe_image.dart b/lib/src/widget/authentication_cached_network_recipe_image.dart index 51110f87..b4e20d82 100644 --- a/lib/src/widget/authentication_cached_network_recipe_image.dart +++ b/lib/src/widget/authentication_cached_network_recipe_image.dart @@ -5,26 +5,28 @@ import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_image.dart'; class AuthenticationCachedNetworkRecipeImage extends StatelessWidget { - final double width; - final double height; - final BoxFit boxFit; + final double? width; + final double? height; + final BoxFit? boxFit; - final int recipeId; + final String recipeId; final bool full; - AuthenticationCachedNetworkRecipeImage( - {@required this.recipeId, - @required this.full, - this.width, - this.height, - this.boxFit}); + const AuthenticationCachedNetworkRecipeImage({ + super.key, + required this.recipeId, + required this.full, + this.width, + this.height, + this.boxFit, + }); @override Widget build(BuildContext context) { - AppAuthentication appAuthentication = - UserRepository().getCurrentAppAuthentication(); + final AppAuthentication appAuthentication = + UserRepository().currentAppAuthentication; - String settings = full ? "full" : "thumb"; + final String settings = full ? "full" : "thumb"; return AuthenticationCachedNetworkImage( url: diff --git a/lib/src/widget/category_card.dart b/lib/src/widget/category_card.dart index 01aba162..31156be7 100644 --- a/lib/src/widget/category_card.dart +++ b/lib/src/widget/category_card.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart' as foundation; import 'package:flutter/material.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; @@ -8,7 +7,10 @@ import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_netw class CategoryCard extends StatelessWidget { final Category category; - const CategoryCard(this.category); + const CategoryCard( + this.category, { + super.key, + }); @override Widget build(BuildContext context) { @@ -21,17 +23,17 @@ class CategoryCard extends StatelessWidget { ShaderMask( blendMode: BlendMode.srcATop, shaderCallback: (bounds) { - return LinearGradient( - begin: Alignment.topCenter, - end: Alignment.center, - colors: [Colors.black, Colors.transparent]) - .createShader(bounds); + return const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.center, + colors: [Colors.black, Colors.transparent], + ).createShader(bounds); }, - child: (category.firstRecipeId != null + child: category.firstRecipeId != null ? ClipRRect( borderRadius: BorderRadius.circular(5), child: AuthenticationCachedNetworkRecipeImage( - recipeId: category.firstRecipeId, + recipeId: category.firstRecipeId!, full: false, boxFit: BoxFit.cover, ), @@ -40,11 +42,11 @@ class CategoryCard extends StatelessWidget { borderRadius: BorderRadius.circular(5), child: Container( color: Colors.grey[400], - child: Center( + child: const Center( child: CircularProgressIndicator(), ), ), - )), + ), ), Padding( padding: const EdgeInsets.all(8.0), @@ -56,8 +58,8 @@ class CategoryCard extends StatelessWidget { color: Colors.white, fontWeight: FontWeight.w500, fontSize: Settings.getValue( - foundation.describeEnum(SettingKeys.category_font_size), - 16, + SettingKeys.category_font_size.name, + defaultValue: 16, ), ), ), @@ -75,10 +77,11 @@ class CategoryCard extends StatelessWidget { border: Border.all(color: Colors.deepOrangeAccent, width: 2), ), child: Center( - child: Text( - category.recipeCount.toString(), - style: TextStyle(color: Colors.white), - )), + child: Text( + category.recipeCount.toString(), + style: const TextStyle(color: Colors.white), + ), + ), ), ), ), diff --git a/lib/src/widget/checkbox_form_field.dart b/lib/src/widget/checkbox_form_field.dart index 6120403b..d8b03185 100644 --- a/lib/src/widget/checkbox_form_field.dart +++ b/lib/src/widget/checkbox_form_field.dart @@ -1,16 +1,13 @@ import 'package:flutter/material.dart'; class CheckboxFormField extends FormField { - CheckboxFormField( - {Widget title, - FormFieldSetter onSaved, - FormFieldValidator validator, - bool initialValue = false, - AutovalidateMode autoValidateMode = AutovalidateMode.disabled}) - : super( - onSaved: onSaved, - validator: validator, - initialValue: initialValue, + CheckboxFormField({ + Widget? title, + super.onSaved, + super.validator, + bool super.initialValue = false, + AutovalidateMode autoValidateMode = AutovalidateMode.disabled, + }) : super( autovalidateMode: autoValidateMode, builder: (FormFieldState state) { return CheckboxListTile( @@ -21,7 +18,7 @@ class CheckboxFormField extends FormField { subtitle: state.hasError ? Builder( builder: (BuildContext context) => Text( - state.errorText, + state.errorText!, style: TextStyle(color: Theme.of(context).errorColor), ), ) diff --git a/lib/src/widget/duration_indicator.dart b/lib/src/widget/duration_indicator.dart index 8c03ceaa..869d8354 100644 --- a/lib/src/widget/duration_indicator.dart +++ b/lib/src/widget/duration_indicator.dart @@ -1,11 +1,14 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class DurationIndicator extends StatelessWidget { final Duration duration; final String name; - const DurationIndicator({@required this.duration, @required this.name}); + const DurationIndicator({ + super.key, + required this.duration, + required this.name, + }); @override Widget build(BuildContext context) { @@ -13,19 +16,10 @@ class DurationIndicator extends StatelessWidget { child: Column( children: [ Container( - child: Center( - child: Padding( - padding: const EdgeInsets.only(left: 13, right: 13), - child: Text( - name, - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), height: 35, decoration: BoxDecoration( color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.only( + borderRadius: const BorderRadius.only( topLeft: Radius.circular(3), topRight: Radius.circular(3), ), @@ -33,17 +27,20 @@ class DurationIndicator extends StatelessWidget { color: Theme.of(context).hintColor, ), ), - ), - Container( child: Center( - child: Text( - "${duration.inHours}:${duration.inMinutes % 60 < 10 ? "0" : ""}${duration.inMinutes % 60}", - style: TextStyle(fontSize: 16), + child: Padding( + padding: const EdgeInsets.only(left: 13, right: 13), + child: Text( + name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), ), ), + ), + Container( height: 35, decoration: BoxDecoration( - borderRadius: BorderRadius.only( + borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(3), bottomRight: Radius.circular(3), ), @@ -51,6 +48,12 @@ class DurationIndicator extends StatelessWidget { color: Theme.of(context).hintColor, ), ), + child: Center( + child: Text( + "${duration.inHours}:${duration.inMinutes % 60 < 10 ? "0" : ""}${duration.inMinutes % 60}", + style: const TextStyle(fontSize: 16), + ), + ), ), ], ), diff --git a/lib/src/widget/input/duration_form_field.dart b/lib/src/widget/input/duration_form_field.dart index 795bc190..06b55935 100644 --- a/lib/src/widget/input/duration_form_field.dart +++ b/lib/src/widget/input/duration_form_field.dart @@ -9,27 +9,25 @@ class DurationFormField extends StatefulWidget { final Duration duration; final void Function(Duration value) onChanged; - const DurationFormField( - {Key key, - @required this.state, - @required this.duration, - @required this.onChanged, - @required this.title}) - : super(key: key); + const DurationFormField({ + super.key, + required this.state, + required this.duration, + required this.onChanged, + required this.title, + }); @override _DurationFormFieldState createState() => _DurationFormFieldState(); } class _DurationFormFieldState extends State { - Duration currentDuration; + late Duration currentDuration; @override void initState() { + currentDuration = widget.duration; super.initState(); - if (currentDuration == null) { - currentDuration = widget.duration; - } } @override @@ -39,7 +37,7 @@ class _DurationFormFieldState extends State { children: [ Text( widget.title, - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), @@ -50,17 +48,19 @@ class _DurationFormFieldState extends State { padding: const EdgeInsets.only(right: 12.0), child: Text(translate('recipe.fields.time.hours')), ), - Container( + SizedBox( width: 70, child: IntegerTextFormField( - enabled: !(widget.state is RecipeUpdateInProgress), - initialValue: - widget.duration != null ? widget.duration.inHours : 0, + enabled: widget.state is! RecipeUpdateInProgress, + initialValue: widget.duration.inHours, decoration: InputDecoration( - hintText: translate('recipe.fields.time.hours')), + hintText: translate('recipe.fields.time.hours'), + ), onChanged: (value) { currentDuration = _updateDuration( - currentDuration: currentDuration, hours: value); + currentDuration: currentDuration, + hours: value, + ); widget.onChanged(currentDuration); }, ), @@ -69,19 +69,20 @@ class _DurationFormFieldState extends State { padding: const EdgeInsets.only(right: 12.0, left: 12.0), child: Text(translate('recipe.fields.time.minutes')), ), - Container( + SizedBox( width: 50, child: IntegerTextFormField( - enabled: !(widget.state is RecipeUpdateInProgress), - initialValue: widget.duration != null - ? widget.duration.inMinutes % 60 - : 0, + enabled: widget.state is! RecipeUpdateInProgress, + initialValue: widget.duration.inMinutes % 60, maxValue: 60, decoration: InputDecoration( - hintText: translate('recipe.fields.time.minutes')), + hintText: translate('recipe.fields.time.minutes'), + ), onChanged: (value) { currentDuration = _updateDuration( - currentDuration: currentDuration, minutes: value); + currentDuration: currentDuration, + minutes: value, + ); widget.onChanged(currentDuration); }, ), @@ -92,22 +93,21 @@ class _DurationFormFieldState extends State { ); } - Duration _updateDuration( - {@required Duration currentDuration, int hours, int minutes}) { + Duration _updateDuration({ + required Duration currentDuration, + int? hours, + int? minutes, + }) { if (hours != null) { - int currentMinutes = 0; - if (currentDuration != null) { - currentMinutes = currentDuration.inMinutes % 60; - } - currentDuration = Duration(hours: hours, minutes: currentMinutes); + final int currentMinutes = currentDuration.inMinutes % 60; + + return Duration(hours: hours, minutes: currentMinutes); } if (minutes != null) { - int currentHours = 0; - if (currentDuration != null) { - currentHours = currentDuration.inHours; - } - currentDuration = Duration(hours: currentHours, minutes: minutes); + final int currentHours = currentDuration.inHours; + + return Duration(hours: currentHours, minutes: minutes); } return currentDuration; diff --git a/lib/src/widget/input/integer_text_form_field.dart b/lib/src/widget/input/integer_text_form_field.dart index bd06cd11..0a9f552e 100644 --- a/lib/src/widget/input/integer_text_form_field.dart +++ b/lib/src/widget/input/integer_text_form_field.dart @@ -2,40 +2,37 @@ import 'package:flutter/material.dart'; class IntegerTextFormField extends StatefulWidget { final int initialValue; - final bool enabled; - final InputDecoration decoration; - final void Function(int value) onChanged; - final void Function(int value) onSaved; - final int minValue; - final int maxValue; + final bool? enabled; + final InputDecoration? decoration; + final void Function(int value)? onChanged; + final void Function(int value)? onSaved; + final int? minValue; + final int? maxValue; - IntegerTextFormField({ - this.initialValue, + const IntegerTextFormField({ + super.key, + this.initialValue = 0, this.enabled, this.decoration, this.onChanged, this.onSaved, this.minValue, this.maxValue, - }) { - assert((this.minValue == null || this.maxValue == null) || - this.minValue <= this.maxValue); - } + }) : assert((minValue == null || maxValue == null) || minValue <= maxValue); @override State createState() => _IntegerTextFormFieldState(); } class _IntegerTextFormFieldState extends State { - TextEditingController controller; + late TextEditingController controller; @override void initState() { super.initState(); - int curVal = _ensureMinMax(widget.initialValue); - if (controller == null) { - controller = new TextEditingController(text: curVal.toString()); - } + final int curVal = _ensureMinMax(widget.initialValue); + controller = TextEditingController(text: curVal.toString()); + controller.addListener(_updateController); } @@ -49,12 +46,12 @@ class _IntegerTextFormFieldState extends State { ); } - _updateController() { - String value = controller.text; - if (value == "") { - widget.onChanged(_ensureMinMax(0)); + void _updateController() { + final String value = controller.text; + if (value.isEmpty) { + widget.onChanged?.call(_ensureMinMax(0)); } else { - var parsedValue = _ensureMinMax(_parseValue(value)); + final parsedValue = _ensureMinMax(_parseValue(value)); if (controller.text != parsedValue.toString()) { controller.value = TextEditingValue( text: parsedValue.toString(), @@ -63,26 +60,25 @@ class _IntegerTextFormFieldState extends State { ), ); } - widget.onChanged(parsedValue); + widget.onChanged?.call(parsedValue); } } int _ensureMinMax(int value) { - if (widget.minValue != null && value < widget.minValue) { - return widget.minValue; - } - if (widget.maxValue != null && value > widget.maxValue) { - return widget.maxValue; - } + final min = widget.minValue; + final max = widget.maxValue; + + if (min != null && value < min) return min; + if (max != null && value > max) return max; return value; } int _parseValue(String input) { - var regexMatches = RegExp(r'(\d+)').allMatches(input); - if (regexMatches == null) { + final regexMatches = RegExp(r'(\d+)').allMatches(input); + if (regexMatches.isEmpty) { return 0; } else { - return int.parse(regexMatches.elementAt(0).group(0)); + return int.parse(regexMatches.elementAt(0).group(0)!); } } } diff --git a/lib/src/widget/input/list_form_field.dart b/lib/src/widget/input/list_form_field.dart index d4a2b7f7..df89104c 100644 --- a/lib/src/widget/input/list_form_field.dart +++ b/lib/src/widget/input/list_form_field.dart @@ -11,52 +11,53 @@ class ListFormField extends StatefulWidget { final List list; final void Function(List value) onChanged; - const ListFormField( - {Key key, - @required this.state, - @required this.list, - @required this.title, - @required this.onChanged}) - : super(key: key); + const ListFormField({ + super.key, + required this.state, + required this.list, + required this.title, + required this.onChanged, + }); @override _ListFormFieldState createState() => _ListFormFieldState(); } class _ListFormFieldState extends State { - List currentList; + late List currentList; @override void initState() { + currentList = widget.list + .map((item) => ListTile(title: Text(item), key: ValueKey(item))) + .toList(); + super.initState(); - if (currentList == null) { - currentList = widget.list.map((item) { - return ListTile(title: Text(item), key: ValueKey(item)); - }).toList(); - } } @override Widget build(BuildContext context) { - return Container( + return SizedBox( height: 56.0 * currentList.length, child: my.ReorderableListView( - children: currentList, onReorder: _onReorder, + children: currentList, ), ); } - _onReorder(int oldIndex, int newIndex) { - ListTile tile = currentList.removeAt(oldIndex); + void _onReorder(int oldIndex, int newIndex) { + final ListTile tile = currentList.removeAt(oldIndex); currentList.insert(newIndex, tile); setState(() { - widget.onChanged(currentList.map((tile) { - Text title = tile.title as Text; - log(title.data); - return title.data; - }).toList()); + widget.onChanged( + currentList.map((tile) { + final Text title = tile.title! as Text; + log(title.data!); + return title.data!; + }).toList(), + ); }); } } diff --git a/lib/src/widget/input/reorderable_list_form_field.dart b/lib/src/widget/input/reorderable_list_form_field.dart index 3d4c21b5..da0b2091 100644 --- a/lib/src/widget/input/reorderable_list_form_field.dart +++ b/lib/src/widget/input/reorderable_list_form_field.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_reorderable_list/flutter_reorderable_list.dart' as RL; +import 'package:flutter_reorderable_list/flutter_reorderable_list.dart' as rl; import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe.dart'; class ReorderableListFormField extends StatefulWidget { @@ -8,13 +8,17 @@ class ReorderableListFormField extends StatefulWidget { final RecipeState state; final Function(List value) onSave; - ReorderableListFormField( - {Key key, this.title, this.items, this.state, this.onSave}) - : super(key: key); + const ReorderableListFormField({ + super.key, + required this.title, + required this.items, + required this.state, + required this.onSave, + }); @override _ReorderableListFormFieldState createState() => - _ReorderableListFormFieldState(items); + _ReorderableListFormFieldState(); } class ItemData { @@ -27,22 +31,17 @@ class ItemData { } class _ReorderableListFormFieldState extends State { - List _items; + final List _items = []; - _ReorderableListFormFieldState(List items) { - _items = List(); - for (int i = 0; i < items.length; ++i) { - _items.add(ItemData(items[i], ValueKey(i))); - } - } + _ReorderableListFormFieldState(); int _indexOfKey(Key key) { return _items.indexWhere((ItemData d) => d.key == key); } bool _reorderCallback(Key item, Key newPosition) { - int draggingIndex = _indexOfKey(item); - int newPositionIndex = _indexOfKey(newPosition); + final int draggingIndex = _indexOfKey(item); + final int newPositionIndex = _indexOfKey(newPosition); final draggedItem = _items[draggingIndex]; setState(() { @@ -52,8 +51,12 @@ class _ReorderableListFormFieldState extends State { return true; } - void _reorderDone(Key item) { - final draggedItem = _items[_indexOfKey(item)]; + @override + void initState() { + for (int i = 0; i < widget.items.length; ++i) { + _items.add(ItemData(widget.items[i], ValueKey(i))); + } + super.initState(); } // @@ -61,6 +64,7 @@ class _ReorderableListFormFieldState extends State { // containing ReorderableItems widgets // + @override Widget build(BuildContext context) { return Column( children: [ @@ -71,12 +75,12 @@ class _ReorderableListFormFieldState extends State { children: [ Text( widget.title, - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), - Container( + SizedBox( width: 1, child: TextFormField( initialValue: "", @@ -89,23 +93,24 @@ class _ReorderableListFormFieldState extends State { ], ), children: [ - RL.ReorderableList( - onReorder: this._reorderCallback, - onReorderDone: this._reorderDone, + rl.ReorderableList( + onReorder: _reorderCallback, child: CustomScrollView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), slivers: [ SliverPadding( padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom), + bottom: MediaQuery.of(context).padding.bottom, + ), sliver: SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { if (index == _items.length) { return Padding( padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 0), + vertical: 8.0, + ), child: Container( decoration: BoxDecoration( color: Theme.of(context).hintColor, @@ -113,14 +118,18 @@ class _ReorderableListFormFieldState extends State { ), child: IconButton( enableFeedback: - !(widget.state is RecipeUpdateInProgress), - icon: Icon(Icons.add), + widget.state is! RecipeUpdateInProgress, + icon: const Icon(Icons.add), onPressed: () { setState(() { - if (!(widget.state - is RecipeUpdateInProgress)) { - _items.add(ItemData( - "", ValueKey(_items.length))); + if (widget.state + is! RecipeUpdateInProgress) { + _items.add( + ItemData( + "", + ValueKey(_items.length), + ), + ); } }); }, @@ -142,15 +151,17 @@ class _ReorderableListFormFieldState extends State { onChange: (String value) { // Mass import with newline separated list if (value.contains("\n")) { - var newItems = List.of(value.split("\n")); + final newItems = List.of(value.split("\n")); _items[index].text = newItems[0]; - var newItemData = List.of( - newItems.getRange(1, newItems.length)) - .asMap() - .entries - .map((e) => ItemData(e.value, - ValueKey(_items.length + e.key))); + final newItemData = List.of( + newItems.getRange(1, newItems.length), + ).asMap().entries.map( + (e) => ItemData( + e.value, + ValueKey(_items.length + e.key), + ), + ); setState(() { _items.insertAll(index + 1, newItemData); }); @@ -175,131 +186,120 @@ class _ReorderableListFormFieldState extends State { } class Item extends StatefulWidget { - Item( - {Key key, - this.data, - this.isFirst, - this.isLast, - this.deleteItem, - this.state, - this.onChange}) - : super(key: key); + const Item({ + super.key, + required this.data, + required this.isFirst, + required this.isLast, + required this.deleteItem, + required this.state, + required this.onChange, + }); final ItemData data; final bool isFirst; final bool isLast; - final Function deleteItem; + final Function() deleteItem; final Function(String value) onChange; final RecipeState state; @override - State createState() => _ItemState( - data: data, - isFirst: isFirst, - isLast: isLast, - deleteItem: deleteItem, - ); + State createState() => _ItemState(); } class _ItemState extends State { - _ItemState({ - this.data, - this.isFirst, - this.isLast, - this.deleteItem, - }); - - final ItemData data; - final bool isFirst; - final bool isLast; - final Function deleteItem; + _ItemState(); - Widget _buildChild(BuildContext context, RL.ReorderableItemState state) { + Widget _buildChild(BuildContext context, rl.ReorderableItemState state) { BoxDecoration decoration; - if (state == RL.ReorderableItemState.dragProxy || - state == RL.ReorderableItemState.dragProxyFinished) { - // slightly transparent background white dragging (just like on iOS) - decoration = BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.8)); - } else { - bool placeholder = state == RL.ReorderableItemState.placeholder; - decoration = BoxDecoration( - border: Border( - top: isFirst && !placeholder - ? Divider.createBorderSide(context) // - : BorderSide.none, - bottom: isLast && placeholder - ? BorderSide.none // - : Divider.createBorderSide(context), - ), - color: placeholder ? null : Theme.of(context).scaffoldBackgroundColor, - ); + switch (state) { + case rl.ReorderableItemState.dragProxy: + case rl.ReorderableItemState.dragProxyFinished: + // slightly transparent background white dragging (just like on iOS) + decoration = BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.8), + ); + break; + default: + final bool placeholder = state == rl.ReorderableItemState.placeholder; + decoration = BoxDecoration( + border: Border( + top: widget.isFirst && !placeholder + ? Divider.createBorderSide(context) // + : BorderSide.none, + bottom: widget.isLast && placeholder + ? BorderSide.none // + : Divider.createBorderSide(context), + ), + color: placeholder ? null : Theme.of(context).scaffoldBackgroundColor, + ); } // For iOS dragging mode, there will be drag handle on the right that triggers // reordering; For android mode it will be just an empty container - Widget dragHandle = RL.ReorderableListener( - canStart: () => !(widget.state is RecipeUpdateInProgress), + final Widget dragHandle = rl.ReorderableListener( + canStart: () => widget.state is! RecipeUpdateInProgress, child: Container( - padding: EdgeInsets.symmetric(vertical: 0, horizontal: 7), - color: Color(0x08000000), - child: Center( + padding: const EdgeInsets.symmetric(horizontal: 7), + color: const Color(0x08000000), + child: const Center( child: Icon(Icons.reorder), ), ), ); - Widget delete = Container( - color: Color(0x08000000), + final Widget delete = ColoredBox( + color: const Color(0x08000000), child: Center( child: IconButton( - enableFeedback: !(widget.state is RecipeUpdateInProgress), - icon: Icon(Icons.delete, color: Colors.red), + enableFeedback: widget.state is! RecipeUpdateInProgress, + icon: const Icon(Icons.delete, color: Colors.red), onPressed: () { - if (!(widget.state is RecipeUpdateInProgress)) { - deleteItem(); + if (widget.state is! RecipeUpdateInProgress) { + widget.deleteItem(); } }, ), ), ); - Widget content = Container( + final Widget content = Container( decoration: decoration, child: SafeArea( - top: false, - bottom: false, - child: Opacity( - // hide content for placeholder - opacity: state == RL.ReorderableItemState.placeholder ? 0.0 : 1.0, - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - vertical: 5.0, - horizontal: 10.0, - ), - child: TextFormField( - enabled: !(widget.state is RecipeUpdateInProgress), - maxLines: 10000, - minLines: 1, - initialValue: data.text, - onChanged: widget.onChange, - autofocus: data.text.isEmpty, - ), + top: false, + bottom: false, + child: Opacity( + // hide content for placeholder + opacity: state == rl.ReorderableItemState.placeholder ? 0.0 : 1.0, + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 5.0, + horizontal: 10.0, + ), + child: TextFormField( + enabled: widget.state is! RecipeUpdateInProgress, + maxLines: 10000, + minLines: 1, + initialValue: widget.data.text, + onChanged: widget.onChange, + autofocus: widget.data.text.isEmpty, ), ), - // Triggers the reordering - dragHandle, - delete, - ], - ), + ), + // Triggers the reordering + dragHandle, + delete, + ], ), - )), + ), + ), + ), ); return content; @@ -307,8 +307,9 @@ class _ItemState extends State { @override Widget build(BuildContext context) { - return RL.ReorderableItem( - key: data.key, // - childBuilder: _buildChild); + return rl.ReorderableItem( + key: widget.data.key, // + childBuilder: _buildChild, + ); } } diff --git a/lib/src/widget/my_reorderable_list.dart b/lib/src/widget/my_reorderable_list.dart index 554ed1d8..6a75b614 100644 --- a/lib/src/widget/my_reorderable_list.dart +++ b/lib/src/widget/my_reorderable_list.dart @@ -2,12 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 - import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter/rendering.dart'; // Examples can assume: @@ -56,27 +53,23 @@ typedef ReorderCallback = void Function(int oldIndex, int newIndex); class ReorderableListView extends StatefulWidget { /// Creates a reorderable list. ReorderableListView({ - Key key, + super.key, this.header, - @required this.children, - @required this.onReorder, + required this.children, + required this.onReorder, this.scrollController, this.scrollDirection = Axis.vertical, this.padding, this.reverse = false, - }) : assert(scrollDirection != null), - assert(onReorder != null), - assert(children != null), - assert( + }) : assert( children.every((Widget w) => w.key != null), 'All children of this widget must have a key.', - ), - super(key: key); + ); /// A non-reorderable header widget to show before the list. /// /// If null, no header will appear before the list. - final Widget header; + final Widget? header; /// The widgets to display. final List children; @@ -93,10 +86,10 @@ class ReorderableListView extends StatefulWidget { /// (via [ScrollController.initialScrollOffset]), reading the current scroll position /// (via [ScrollController.offset]), or changing it (via [ScrollController.jumpTo] or /// [ScrollController.animateTo]). - final ScrollController scrollController; + final ScrollController? scrollController; /// The amount of space by which to inset the [children]. - final EdgeInsets padding; + final EdgeInsets? padding; /// Whether the scroll view scrolls in the reading direction. /// @@ -138,7 +131,7 @@ class _ReorderableListViewState extends State { GlobalKey(debugLabel: '$ReorderableListView overlay key'); // This entry contains the scrolling list itself. - OverlayEntry _listOverlayEntry; + late OverlayEntry _listOverlayEntry; @override void initState() { @@ -148,12 +141,12 @@ class _ReorderableListViewState extends State { builder: (BuildContext context) { return _ReorderableListContent( header: widget.header, - children: widget.children, scrollController: widget.scrollController, scrollDirection: widget.scrollDirection, onReorder: widget.onReorder, padding: widget.padding, reverse: widget.reverse, + children: widget.children, ); }, ); @@ -161,9 +154,12 @@ class _ReorderableListViewState extends State { @override Widget build(BuildContext context) { - return Overlay(key: _overlayKey, initialEntries: [ - _listOverlayEntry, - ]); + return Overlay( + key: _overlayKey, + initialEntries: [ + _listOverlayEntry, + ], + ); } } @@ -171,20 +167,20 @@ class _ReorderableListViewState extends State { // ReorderableListView. class _ReorderableListContent extends StatefulWidget { const _ReorderableListContent({ - @required this.header, - @required this.children, - @required this.scrollController, - @required this.scrollDirection, - @required this.padding, - @required this.onReorder, - @required this.reverse, + required this.header, + required this.children, + required this.scrollController, + required this.scrollDirection, + required this.padding, + required this.onReorder, + required this.reverse, }); - final Widget header; + final Widget? header; final List children; - final ScrollController scrollController; + final ScrollController? scrollController; final Axis scrollDirection; - final EdgeInsets padding; + final EdgeInsets? padding; final ReorderCallback onReorder; final bool reverse; @@ -212,22 +208,22 @@ class _ReorderableListContentState extends State<_ReorderableListContent> static const Duration _scrollAnimationDuration = Duration(milliseconds: 200); // Controls scrolls and measures scroll progress. - ScrollController _scrollController; + late ScrollController _scrollController; // This controls the entrance of the dragging widget into a new place. - AnimationController _entranceController; + late AnimationController _entranceController; // This controls the 'ghost' of the dragging widget, which is left behind // where the widget used to be. - AnimationController _ghostController; + late AnimationController _ghostController; // The member of widget.children currently being dragged. // // Null if no drag is underway. - Key _dragging; + Key? _dragging; // The last computed size of the feedback widget being dragged. - Size _draggingFeedbackSize; + Size? _draggingFeedbackSize; // The location that the dragging widget occupied before it started to drag. int _dragStartIndex = 0; @@ -252,11 +248,11 @@ class _ReorderableListContentState extends State<_ReorderableListContent> double dropAreaWithoutMargin; switch (widget.scrollDirection) { case Axis.horizontal: - dropAreaWithoutMargin = _draggingFeedbackSize.width; + dropAreaWithoutMargin = _draggingFeedbackSize!.width; break; case Axis.vertical: default: - dropAreaWithoutMargin = _draggingFeedbackSize.height; + dropAreaWithoutMargin = _draggingFeedbackSize!.height; break; } return dropAreaWithoutMargin + _dropAreaMargin; @@ -312,10 +308,9 @@ class _ReorderableListContentState extends State<_ReorderableListContent> // Scrolls to a target context if that context is not on the screen. void _scrollTo(BuildContext context) { if (_scrolling) return; - final RenderObject contextObject = context.findRenderObject(); - final RenderAbstractViewport viewport = - RenderAbstractViewport.of(contextObject); - assert(viewport != null); + final contextObject = context.findRenderObject(); + final viewport = RenderAbstractViewport.of(contextObject); + assert(contextObject != null && viewport != null); // If and only if the current scroll offset falls in-between the offsets // necessary to reveal the selected context at the top or bottom of the // screen, then it is already on-screen. @@ -323,7 +318,7 @@ class _ReorderableListContentState extends State<_ReorderableListContent> final double scrollOffset = _scrollController.offset; final double topOffset = max( _scrollController.position.minScrollExtent, - viewport.getOffsetToReveal(contextObject, 0.0).offset - margin, + viewport!.getOffsetToReveal(contextObject!, 0.0).offset - margin, ); final double bottomOffset = min( _scrollController.position.maxScrollExtent, @@ -341,7 +336,7 @@ class _ReorderableListContentState extends State<_ReorderableListContent> duration: _scrollAnimationDuration, curve: Curves.easeInOut, ) - .then((void value) { + .then((_) { setState(() { _scrolling = false; }); @@ -351,13 +346,13 @@ class _ReorderableListContentState extends State<_ReorderableListContent> // Wraps children in Row or Column, so that the children flow in // the widget's scrollDirection. - Widget _buildContainerForScrollDirection({List children}) { + Widget _buildContainerForScrollDirection({List? children}) { switch (widget.scrollDirection) { case Axis.horizontal: - return Row(children: children); + return Row(children: children!); case Axis.vertical: default: - return Column(children: children); + return Column(children: children!); } } @@ -366,7 +361,7 @@ class _ReorderableListContentState extends State<_ReorderableListContent> Widget _wrap(Widget toWrap, int index, BoxConstraints constraints) { assert(toWrap.key != null); final _ReorderableListViewChildGlobalKey keyIndexGlobalKey = - _ReorderableListViewChildGlobalKey(toWrap.key, this); + _ReorderableListViewChildGlobalKey(toWrap.key!, this); // We pass the toWrapWithGlobalKey into the Draggable so that when a list // item gets dragged, the accessibility framework can preserve the selected // state of the dragging item. @@ -379,7 +374,7 @@ class _ReorderableListContentState extends State<_ReorderableListContent> _ghostIndex = index; _currentIndex = index; _entranceController.value = 1.0; - _draggingFeedbackSize = keyIndexGlobalKey.currentContext.size; + _draggingFeedbackSize = keyIndexGlobalKey.currentContext?.size; }); } @@ -418,7 +413,8 @@ class _ReorderableListContentState extends State<_ReorderableListContent> // If the item can move to before its current position in the list. if (index > 0) { semanticsActions[CustomSemanticsAction( - label: localizations.reorderItemToStart)] = moveToStart; + label: localizations.reorderItemToStart, + )] = moveToStart; String reorderItemBefore = localizations.reorderItemUp; if (widget.scrollDirection == Axis.horizontal) { reorderItemBefore = Directionality.of(context) == TextDirection.ltr @@ -461,8 +457,11 @@ class _ReorderableListContentState extends State<_ReorderableListContent> ); } - Widget buildDragTarget(BuildContext context, List acceptedCandidates, - List rejectedCandidates) { + Widget buildDragTarget( + BuildContext context, + List acceptedCandidates, + List rejectedCandidates, + ) { final Widget toWrapWithSemantics = wrapWithSemantics(); // We build the draggable inside of a layout builder so that we can @@ -481,10 +480,9 @@ class _ReorderableListContentState extends State<_ReorderableListContent> child: toWrapWithSemantics, ), ), - child: _dragging == toWrap.key ? const SizedBox() : toWrapWithSemantics, childWhenDragging: const SizedBox(), - dragAnchor: DragAnchor.child, onDragStarted: onDragStarted, + dragAnchorStrategy: childDragAnchorStrategy, // When the drag ends inside a DragTarget widget, the drag // succeeds, and we reorder the widget into position appropriately. onDragCompleted: onDragEnded, @@ -494,6 +492,7 @@ class _ReorderableListContentState extends State<_ReorderableListContent> onDraggableCanceled: (Velocity velocity, Offset offset) { onDragEnded(); }, + child: _dragging == toWrap.key ? const SizedBox() : toWrapWithSemantics, ); // The target for dropping at the end of the list doesn't need to be @@ -517,47 +516,51 @@ class _ReorderableListContentState extends State<_ReorderableListContent> // We open up a space under where the dragging widget currently is to // show it can be dropped. if (_currentIndex == index) { - return _buildContainerForScrollDirection(children: [ - SizeTransition( - sizeFactor: _entranceController, - axis: widget.scrollDirection, - child: spacing, - ), - child, - ]); + return _buildContainerForScrollDirection( + children: [ + SizeTransition( + sizeFactor: _entranceController, + axis: widget.scrollDirection, + child: spacing, + ), + child, + ], + ); } // We close up the space under where the dragging widget previously was // with the ghostController animation. if (_ghostIndex == index) { - return _buildContainerForScrollDirection(children: [ - SizeTransition( - sizeFactor: _ghostController, - axis: widget.scrollDirection, - child: spacing, - ), - child, - ]); + return _buildContainerForScrollDirection( + children: [ + SizeTransition( + sizeFactor: _ghostController, + axis: widget.scrollDirection, + child: spacing, + ), + child, + ], + ); } return child; } // We wrap the drag target in a Builder so that we can scroll to its specific context. - return Builder(builder: (BuildContext context) { - return DragTarget( - builder: buildDragTarget, - onWillAccept: (Key toAccept) { - setState(() { - _nextIndex = index; - _requestAnimationToNextIndex(); - }); - _scrollTo(context); - // If the target is not the original starting point, then we will accept the drop. - return _dragging == toAccept && toAccept != toWrap.key; - }, - onAccept: (Key accepted) {}, - onLeave: (Object leaving) {}, - ); - }); + return Builder( + builder: (BuildContext context) { + return DragTarget( + builder: buildDragTarget, + onWillAccept: (Key? toAccept) { + setState(() { + _nextIndex = index; + _requestAnimationToNextIndex(); + }); + _scrollTo(context); + // If the target is not the original starting point, then we will accept the drop. + return _dragging == toAccept && toAccept != toWrap.key; + }, + ); + }, + ); } @override @@ -565,49 +568,50 @@ class _ReorderableListContentState extends State<_ReorderableListContent> assert(debugCheckHasMaterialLocalizations(context)); // We use the layout builder to constrain the cross-axis size of dragging child widgets. return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - const Key endWidgetKey = Key('DraggableList - End Widget'); - Widget finalDropArea; - switch (widget.scrollDirection) { - case Axis.horizontal: - finalDropArea = SizedBox( - key: endWidgetKey, - width: _defaultDropAreaExtent, - height: constraints.maxHeight, - ); - break; - case Axis.vertical: - default: - finalDropArea = SizedBox( - key: endWidgetKey, - height: _defaultDropAreaExtent, - width: constraints.maxWidth, - ); - break; - } + builder: (BuildContext context, BoxConstraints constraints) { + const Key endWidgetKey = Key('DraggableList - End Widget'); + Widget finalDropArea; + switch (widget.scrollDirection) { + case Axis.horizontal: + finalDropArea = SizedBox( + key: endWidgetKey, + width: _defaultDropAreaExtent, + height: constraints.maxHeight, + ); + break; + case Axis.vertical: + default: + finalDropArea = SizedBox( + key: endWidgetKey, + height: _defaultDropAreaExtent, + width: constraints.maxWidth, + ); + break; + } - // If the reorderable list only has one child element, reordering - // should not be allowed. - final bool hasMoreThanOneChildElement = widget.children.length > 1; - - return ListView( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - scrollDirection: widget.scrollDirection, - padding: widget.padding, - controller: _scrollController, - reverse: widget.reverse, - children: [ - if (widget.reverse && hasMoreThanOneChildElement) - _wrap(finalDropArea, widget.children.length, constraints), - if (widget.header != null) widget.header, - for (int i = 0; i < widget.children.length; i += 1) - _wrap(widget.children[i], i, constraints), - if (!widget.reverse && hasMoreThanOneChildElement) - _wrap(finalDropArea, widget.children.length, constraints), - ], - ); - }); + // If the reorderable list only has one child element, reordering + // should not be allowed. + final bool hasMoreThanOneChildElement = widget.children.length > 1; + + return ListView( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + scrollDirection: widget.scrollDirection, + padding: widget.padding, + controller: _scrollController, + reverse: widget.reverse, + children: [ + if (widget.reverse && hasMoreThanOneChildElement) + _wrap(finalDropArea, widget.children.length, constraints), + if (widget.header != null) widget.header!, + for (int i = 0; i < widget.children.length; i += 1) + _wrap(widget.children[i], i, constraints), + if (!widget.reverse && hasMoreThanOneChildElement) + _wrap(finalDropArea, widget.children.length, constraints), + ], + ); + }, + ); } } @@ -634,5 +638,5 @@ class _ReorderableListViewChildGlobalKey extends GlobalObjectKey { } @override - int get hashCode => hashValues(subKey, state); + int get hashCode => Object.hash(subKey, state); } diff --git a/pubspec.yaml b/pubspec.yaml index 7d65ce75..04a65f5b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,9 @@ name: nextcloud_cookbook_flutter -description: A new Flutter application. +description: A flutter client for the Nextcloud CookBook app + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -11,10 +15,10 @@ description: A new Flutter application. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.7.8+23 +version: 0.7.9+24 environment: - sdk: ">=2.10.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: 3.3.0 dependencies: @@ -30,7 +34,7 @@ dependencies: punycode: ^1.0.0 #for app key - flutter_secure_storage: ^5.0.2 + flutter_secure_storage: ^7.0.1 url_launcher: ^6.0.15 # The following adds the Cupertino Icons font to your application. @@ -47,13 +51,13 @@ dependencies: equatable: ^2.0.0 # Search Field (For Category & Tags) - searchfield: ^0.6.2 + searchfield: ^0.7.2 # Search search_page: ^2.0.0 # Settings Page - flutter_settings_screens: ^0.2.2+1 + flutter_settings_screens: ^0.3.3-null-safety+2 theme_mode_handler: ^3.0.0 @@ -63,7 +67,7 @@ dependencies: # Screen always on wakelock: ^0.6.1 - xml: ^5.1.0 + xml: ^6.1.0 flutter_spinkit: ^5.0.0 validators: ^3.0.0 @@ -76,19 +80,27 @@ dependencies: flutter_reorderable_list: 1.2.0 # Timer for cooking time - flutter_local_notifications: ^9.4.0 + flutter_local_notifications: ^13.0.0 flutter_native_timezone: 2.0.0 + timezone: ^0.9.1 cached_network_image: ^3.0.0 + flutter_cache_manager: ^3.3.0 + + flutter_typeahead: 4.1.1 - flutter_typeahead: 4.1.0 + copy_with_extension: ^5.0.0 dev_dependencies: - flutter_launcher_icons: ^0.9.0 + flutter_launcher_icons: ^0.11.0 + + copy_with_extension_gen: ^5.0.0 + build_runner: ^2.3.0 flutter_test: sdk: flutter + lint: ^2.0.0 flutter_icons: android: "launcher_icon" @@ -102,7 +114,6 @@ flutter_icons: # The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class.