diff --git a/lib/main.dart b/lib/main.dart index 91e354e5..1fcf4baa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -107,13 +107,49 @@ class _AppState extends State { case AuthenticationStatus.loading: return const SplashPage(); case AuthenticationStatus.authenticated: - IntentRepository().handleIntent(); - final categoryBloc = BlocProvider.of(context); - if (categoryBloc.state.status == - CategoriesStatus.loadInProgress) { - categoryBloc.add(const CategoriesLoaded()); - } - return const CategoryScreen(); + return FutureBuilder( + future: UserRepository().fetchApiVersion(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Container(); + } + + if (snapshot.hasError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + translate( + "categories.errors.api_version_check_failed", + args: {"error_msg": snapshot.error}, + ), + ), + backgroundColor: Colors.red, + ), + ); + } + + if (!UserRepository().isVersionSupported(snapshot.data!)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + translate( + "categories.errors.api_version_above_confirmed", + args: { + "version": + "${snapshot.data!.major}.${snapshot.data!.minor}" + }, + ), + ), + backgroundColor: Colors.orange, + ), + ); + } + + IntentRepository().handleIntent(); + return const CategoryScreen(); + }, + ); + case AuthenticationStatus.unauthenticated: return const LoginScreen(); case AuthenticationStatus.invalid: diff --git a/lib/src/blocs/authentication/authentication_bloc.dart b/lib/src/blocs/authentication/authentication_bloc.dart index b9e076bb..0343ff78 100644 --- a/lib/src/blocs/authentication/authentication_bloc.dart +++ b/lib/src/blocs/authentication/authentication_bloc.dart @@ -24,9 +24,15 @@ class AuthenticationBloc if (hasToken) { await userRepository.loadAppAuthentication(); - bool validCredentials = false; try { - validCredentials = await userRepository.checkAppAuthentication(); + final validCredentials = await userRepository.checkAppAuthentication(); + + if (validCredentials) { + emit(AuthenticationState(status: AuthenticationStatus.authenticated)); + } else { + await userRepository.deleteAppAuthentication(); + emit(AuthenticationState(status: AuthenticationStatus.invalid)); + } } catch (e) { emit( AuthenticationState( @@ -34,14 +40,6 @@ class AuthenticationBloc error: e.toString(), ), ); - return; - } - if (validCredentials) { - await userRepository.fetchApiVersion(); - emit(AuthenticationState(status: AuthenticationStatus.authenticated)); - } else { - await userRepository.deleteAppAuthentication(); - emit(AuthenticationState(status: AuthenticationStatus.invalid)); } } else { emit(AuthenticationState(status: AuthenticationStatus.unauthenticated)); @@ -54,7 +52,6 @@ class AuthenticationBloc ) async { emit(AuthenticationState()); await userRepository.persistAppAuthentication(event.appAuthentication); - await userRepository.fetchApiVersion(); emit(AuthenticationState(status: AuthenticationStatus.authenticated)); } diff --git a/lib/src/blocs/authentication/authentication_state.dart b/lib/src/blocs/authentication/authentication_state.dart index 7899f347..4e02dd59 100644 --- a/lib/src/blocs/authentication/authentication_state.dart +++ b/lib/src/blocs/authentication/authentication_state.dart @@ -1,10 +1,23 @@ part of 'authentication_bloc.dart'; enum AuthenticationStatus { + /// The user has not been authenticated unauthenticated, + + /// The user has been authenticated authenticated, + + /// The provided authentication is invalid invalid, + + /// Loading + /// + /// Can either: + /// - retrive saved authentication + /// - log in witht he provided authentication loading, + + /// An error accured while authenticating error; } diff --git a/lib/src/blocs/categories/categories_bloc.dart b/lib/src/blocs/categories/categories_bloc.dart index 2d41ef5b..0ae589cc 100644 --- a/lib/src/blocs/categories/categories_bloc.dart +++ b/lib/src/blocs/categories/categories_bloc.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; part 'categories_event.dart'; @@ -18,20 +18,19 @@ class CategoriesBloc extends Bloc { Emitter emit, ) async { try { - final List categories = await dataRepository.fetchCategories(); - dataRepository.updateCategoryNames(categories); + final categories = await dataRepository.fetchCategories(); emit( CategoriesState( status: CategoriesStatus.loadSuccess, categories: categories, ), ); - final List categoriesWithImage = - await dataRepository.fetchCategoryMainRecipes(categories); + final recipes = await dataRepository.fetchCategoryMainRecipes(categories); emit( CategoriesState( status: CategoriesStatus.imageLoadSuccess, - categories: categoriesWithImage, + categories: categories, + recipes: recipes, ), ); } on Exception catch (e) { diff --git a/lib/src/blocs/categories/categories_state.dart b/lib/src/blocs/categories/categories_state.dart index 985129b0..8ebf1bff 100644 --- a/lib/src/blocs/categories/categories_state.dart +++ b/lib/src/blocs/categories/categories_state.dart @@ -11,22 +11,26 @@ class CategoriesState extends Equatable { final CategoriesStatus status; final String? error; final Iterable? categories; + final Iterable? recipes; CategoriesState({ this.status = CategoriesStatus.loadInProgress, this.error, this.categories, + this.recipes, }) { switch (status) { case CategoriesStatus.loadInProgress: - assert(error == null && categories == null); + assert(error == null && categories == null && recipes == null); break; case CategoriesStatus.loadSuccess: + assert(error == null && categories != null && recipes == null); + break; case CategoriesStatus.imageLoadSuccess: - assert(error == null && categories != null); + assert(error == null && categories != null && recipes != null); break; case CategoriesStatus.loadFailure: - assert(error != null && categories == null); + assert(error != null && categories == null && recipes == null); } } diff --git a/lib/src/blocs/recipe/recipe_bloc.dart b/lib/src/blocs/recipe/recipe_bloc.dart index 6e46932f..b080f24c 100644 --- a/lib/src/blocs/recipe/recipe_bloc.dart +++ b/lib/src/blocs/recipe/recipe_bloc.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; part 'recipe_event.dart'; diff --git a/lib/src/blocs/recipes_short/recipes_short_bloc.dart b/lib/src/blocs/recipes_short/recipes_short_bloc.dart index d74747f4..45b17f3f 100644 --- a/lib/src/blocs/recipes_short/recipes_short_bloc.dart +++ b/lib/src/blocs/recipes_short/recipes_short_bloc.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; part 'recipes_short_event.dart'; diff --git a/lib/src/models/category.dart b/lib/src/models/category.dart deleted file mode 100644 index e050793e..00000000 --- a/lib/src/models/category.dart +++ /dev/null @@ -1,44 +0,0 @@ -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; - final String? firstRecipeId; - - const Category(this.name, this.recipeCount) : firstRecipeId = null; - - const Category._({ - required this.name, - required this.recipeCount, - required this.firstRecipeId, - }); - - Category.fromJson(Map json) - : name = json["name"] as String, - recipeCount = json["recipe_count"] is int - ? json["recipe_count"] as int - : int.parse(json["recipe_count"] as String), - firstRecipeId = null; - - @override - List get props => [name]; - - static List parseCategories(String responseBody) { - final parsed = json.decode(responseBody) as List; - - return parsed - .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 deleted file mode 100644 index 72cd498c..00000000 --- a/lib/src/models/category.g.dart +++ /dev/null @@ -1,59 +0,0 @@ -// 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/image_response.dart b/lib/src/models/image_response.dart new file mode 100644 index 00000000..be65a836 --- /dev/null +++ b/lib/src/models/image_response.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; + +class ImageResponse { + final Uint8List data; + final bool isSvg; + + const ImageResponse({ + required this.data, + required this.isSvg, + }); +} diff --git a/lib/src/models/intial_login.dart b/lib/src/models/intial_login.dart deleted file mode 100644 index e8dda40f..00000000 --- a/lib/src/models/intial_login.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:nextcloud_cookbook_flutter/src/models/poll.dart'; - -class InitialLogin { - Poll poll; - String login; - - InitialLogin({ - required this.poll, - required this.login, - }); - - factory InitialLogin.fromJson(Map json) => InitialLogin( - poll: Poll.fromJson(json["poll"] as Map), - login: json["login"] as String, - ); - - Map toJson() => { - "poll": poll.toJson(), - "login": login, - }; -} diff --git a/lib/src/models/poll.dart b/lib/src/models/poll.dart deleted file mode 100644 index 43a78084..00000000 --- a/lib/src/models/poll.dart +++ /dev/null @@ -1,19 +0,0 @@ -class Poll { - String token; - String endpoint; - - Poll({ - required this.token, - required this.endpoint, - }); - - factory Poll.fromJson(Map json) => Poll( - token: json["token"] as String, - endpoint: json["endpoint"] as String, - ); - - Map toJson() => { - "token": token, - "endpoint": endpoint, - }; -} diff --git a/lib/src/models/recipe.dart b/lib/src/models/recipe.dart index 52fa62d8..942afeea 100644 --- a/lib/src/models/recipe.dart +++ b/lib/src/models/recipe.dart @@ -1,287 +1,45 @@ -import 'dart:convert'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; -import 'package:equatable/equatable.dart'; -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 String id; - final String name; - final String imageUrl; - final String recipeCategory; - final String description; - final Map nutrition; - final List recipeIngredient; - final List recipeInstructions; - final List tool; - final int recipeYield; - final Duration prepTime; - final Duration cookTime; - final Duration totalTime; - final String keywords; - final String image; - final String url; - final Map remainingData; - - factory Recipe.fromJsonString(String jsonString) => - Recipe.fromJson(json.decode(jsonString) as Map); - - factory Recipe.fromJson(Map data) { - final String id = - data["id"] is int ? data["id"]!.toString() : data["id"] as String; - final String name = data["name"] as String; - 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), - ); - data["nutrition"] = (data["nutrition"] as Map) - .map((key, value) => MapEntry(key, value?.toString())) - ..removeWhere( - (key, value) => NutritionUtility.nutritionProperties.contains(key), - ); +extension RecipeExtension on Recipe { + Map get nutritionList { + final Map items = {}; + if (nutrition.calories != null) { + items["calories"] = nutrition.calories!; } - - List recipeIngredient = []; - if (data["recipeIngredient"] is Map) { - (data["recipeIngredient"] as Map) - .forEach((k, v) => recipeIngredient.add(v as String)); - } else if (data["recipeIngredient"] != null) { - recipeIngredient = (data["recipeIngredient"] as List).cast(); + if (nutrition.carbohydrateContent != null) { + items["carbohydrateContent"] = nutrition.carbohydrateContent!; } - - List recipeInstructions = []; - if (data["recipeInstructions"] is Map) { - (data["recipeInstructions"] as Map) - .forEach((k, v) => recipeInstructions.add(v as String)); - } else if (data["recipeInstructions"] != null) { - recipeInstructions = (data["recipeInstructions"] as List).cast(); + if (nutrition.cholesterolContent != null) { + items["cholesterolContent"] = nutrition.cholesterolContent!; } - - List tool = []; - if (data["tool"] is Map) { - (data["tool"] as Map).forEach((k, v) => tool.add(v as String)); - } else if (data["tool"] != null) { - tool = (data["tool"] as List).cast(); + if (nutrition.fatContent != null) { + items["fatContent"] = nutrition.fatContent!; } - - final int recipeYield = data["recipeYield"] as int? ?? 1; - final Duration prepTime = data.containsKey("prepTime") && - data["prepTime"] != "" && - data["prepTime"] != null - ? IsoTimeFormat.toDuration(data["prepTime"] as String) - : Duration.zero; - final Duration cookTime = data.containsKey("cookTime") && - data["cookTime"] != "" && - data["cookTime"] != null - ? IsoTimeFormat.toDuration(data["cookTime"] as String) - : Duration.zero; - final Duration totalTime = data.containsKey("totalTime") && - data["totalTime"] != "" && - data["totalTime"] != null - ? 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"); - data.remove("imageUrl"); - data.remove("recipeCategory"); - data.remove("description"); - // Nutrition items are filtered at the point of parsing - data.remove("recipeIngredient"); - data.remove("recipeInstructions"); - data.remove("tool"); - data.remove("recipeYield"); - data.remove("prepTime"); - data.remove("cookTime"); - data.remove("totalTime"); - data.remove("keywords"); - data.remove("image"); - data.remove("url"); - - data.remove("dateModified"); - - return Recipe._( - id, - name, - imageUrl, - recipeCategory, - description, - recipeNutrition, - recipeIngredient, - recipeInstructions, - tool, - recipeYield, - prepTime, - cookTime, - totalTime, - keywords, - image, - url, - data, - ); - } - - 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 toJsonString() => jsonEncode(toJson()); - - Map toJson() { - final Map updatedData = { - 'id': id, - 'name': name, - 'imageUrl': imageUrl, - 'recipeCategory': recipeCategory, - 'description': description, - 'recipeIngredient': recipeIngredient, - 'recipeInstructions': recipeInstructions, - 'tool': tool, - 'recipeYield': recipeYield, - 'prepTime': _durationToIso(prepTime), - 'cookTime': _durationToIso(cookTime), - 'totalTime': _durationToIso(totalTime), - 'keywords': keywords, - 'image': image, - 'url': url, - 'dateModified': DateTime.now().toIso8601String() - }; - - // Add all the data points that are not handled by the app! - remainingData.addAll(updatedData); - - if (remainingData['nutrition'] is Map) { - (remainingData['nutrition'] as Map).addAll(nutrition); - } else { - remainingData['nutrition'] = nutrition; + if (nutrition.fiberContent != null) { + items["fiberContent"] = nutrition.fiberContent!; } - - return remainingData; - } - - MutableRecipe toMutableRecipe() { - final MutableRecipe mutableRecipe = MutableRecipe(); - - 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]; - - String _durationToIso(Duration? duration) { - if (duration != null && duration.inMinutes != 0) { - return "PT${duration.inHours}H${duration.inMinutes % 60}M"; - } else { - return ""; + if (nutrition.proteinContent != null) { + items["proteinContent"] = nutrition.proteinContent!; + } + if (nutrition.saturatedFatContent != null) { + items["saturatedFatContent"] = nutrition.saturatedFatContent!; + } + if (nutrition.servingSize != null) { + items["servingSize"] = nutrition.servingSize!; + } + if (nutrition.sodiumContent != null) { + items["sodiumContent"] = nutrition.sodiumContent!; + } + if (nutrition.sugarContent != null) { + items["sugarContent"] = nutrition.sugarContent!; + } + if (nutrition.transFatContent != null) { + items["transFatContent"] = nutrition.transFatContent!; + } + if (nutrition.unsaturatedFatContent != null) { + items["unsaturatedFatContent"] = nutrition.unsaturatedFatContent!; } - } -} - -class MutableRecipe { - 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._( - id, - name, - imageUrl, - recipeCategory, - description, - nutrition, - recipeIngredient, - recipeInstructions, - tool, - recipeYield, - prepTime, - cookTime, - totalTime, - keywords, - image, - url, - remainingData, - ); + return items; } } diff --git a/lib/src/models/recipe_short.dart b/lib/src/models/recipe_short.dart deleted file mode 100644 index 635fbb9e..00000000 --- a/lib/src/models/recipe_short.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; - -class RecipeStub extends Equatable { - final String _recipeId; - final String _name; - final String _imageUrl; - - String get recipeId => _recipeId; - String get name => _name; - String get imageUrl => _imageUrl; - - RecipeStub.fromJson(Map json) - : _recipeId = json["recipe_id"] is int - ? json["recipe_id"]!.toString() - : json["recipe_id"] as String, - _name = json["name"] as String, - _imageUrl = json["imageUrl"] as String; - - static List parseRecipesShort(String responseBody) { - final parsed = json.decode(responseBody) as List; - - return parsed - .map( - (json) => RecipeStub.fromJson(json as Map), - ) - .toList(); - } - - @override - List get props => [_recipeId]; -} diff --git a/lib/src/models/timer.dart b/lib/src/models/timer.dart index 12e46306..be2a1856 100644 --- a/lib/src/models/timer.dart +++ b/lib/src/models/timer.dart @@ -1,13 +1,17 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; part 'timer.g.dart'; @JsonSerializable(constructor: "restore") class Timer { @visibleForTesting + @JsonKey( + toJson: _recipeToJson, + fromJson: _recipeFromJson, + ) final Recipe? recipe; final DateTime done; @@ -23,7 +27,7 @@ class Timer { _body = null, _duration = null, _recipeId = null, - done = DateTime.now().add(recipe.cookTime); + done = DateTime.now().add(recipe.cookTime!); // Restore Timer fom pending notification @visibleForTesting @@ -68,8 +72,8 @@ class Timer { String get body => _body ?? "$title ${translate('timer.finished')}"; String get title => _title ?? recipe!.name; - Duration get duration => _duration ?? recipe!.cookTime; - String get recipeId => _recipeId ?? recipe!.id; + Duration get duration => _duration ?? recipe!.cookTime!; + String get recipeId => _recipeId ?? recipe!.id!; /// The remaining time of the timer /// @@ -110,3 +114,9 @@ class Timer { recipeId, ); } + +Recipe _recipeFromJson(String data) => + standardSerializers.fromJson(Recipe.serializer, data)!; + +String? _recipeToJson(Object? data) => + data != null ? standardSerializers.toJson(Recipe.serializer, data) : null; diff --git a/lib/src/models/timer.g.dart b/lib/src/models/timer.g.dart index b59076f4..b2a9cd98 100644 --- a/lib/src/models/timer.g.dart +++ b/lib/src/models/timer.g.dart @@ -7,13 +7,13 @@ part of 'timer.dart'; // ************************************************************************** Timer _$TimerFromJson(Map json) => Timer.restore( - Recipe.fromJson(json['recipe'] as Map), + _recipeFromJson(json['recipe'] as String), DateTime.parse(json['done'] as String), json['id'] as int?, ); Map _$TimerToJson(Timer instance) => { - 'recipe': instance.recipe, + 'recipe': _recipeToJson(instance.recipe), 'done': instance.done.toIso8601String(), 'id': instance.id, }; diff --git a/lib/src/screens/category/category_screen.dart b/lib/src/screens/category/category_screen.dart index 22d7d5ce..50fea4ca 100644 --- a/lib/src/screens/category/category_screen.dart +++ b/lib/src/screens/category/category_screen.dart @@ -3,22 +3,19 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/categories/categories_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe_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'; import 'package:nextcloud_cookbook_flutter/src/screens/timer_screen.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; -import 'package:nextcloud_cookbook_flutter/src/widget/api_version_warning.dart'; -import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_image.dart'; -import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/category_card.dart'; +import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; +import 'package:nextcloud_cookbook_flutter/src/widget/user_image.dart'; import 'package:search_page/search_page.dart'; class CategoryScreen extends StatefulWidget { @@ -56,13 +53,8 @@ class _CategoryScreenState extends State { decoration: BoxDecoration( color: Theme.of(context).primaryColor, ), - child: Center( - child: ClipOval( - child: AuthenticationCachedNetworkImage( - url: DataRepository().getUserAvatarUrl(), - boxFit: BoxFit.fill, - ), - ), + child: const Center( + child: UserImage(), ), ), ListTile( @@ -157,10 +149,9 @@ class _CategoryScreenState extends State { ], builder: (recipe) => ListTile( title: Text(recipe.name), - trailing: AuthenticationCachedNetworkRecipeImage( - recipeId: recipe.recipeId, - full: false, - width: 50, + trailing: RecipeImage( + id: recipe.recipeId, + size: const Size.square(50), ), onTap: () => Navigator.of(context).pushReplacement( @@ -234,18 +225,18 @@ class _CategoryScreenState extends State { case CategoriesStatus.loadSuccess: return _buildCategoriesScreen(categoriesState.categories!); case CategoriesStatus.imageLoadSuccess: - return _buildCategoriesScreen(categoriesState.categories!); + return _buildCategoriesScreen( + categoriesState.categories!, + categoriesState.recipes, + ); case CategoriesStatus.loadInProgress: - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: SpinKitWave( - color: Theme.of(context).primaryColor, - ), - ), - const ApiVersionWarning(), - ], + BlocProvider.of(context) + .add(const CategoriesLoaded()); + + return Center( + child: SpinKitWave( + color: Theme.of(context).primaryColor, + ), ); case CategoriesStatus.loadFailure: return Padding( @@ -277,8 +268,9 @@ class _CategoryScreenState extends State { } Widget _buildCategoriesScreen( - Iterable categories, - ) { + Iterable categories, [ + Iterable? recipe, + ]) { final double screenWidth = MediaQuery.of(context).size.width; final int axisRatio = (screenWidth / 150).round(); final int axisCount = axisRatio < 1 ? 1 : axisRatio; @@ -291,21 +283,25 @@ class _CategoryScreenState extends State { mainAxisSpacing: 10, padding: const EdgeInsets.only(top: 10), semanticChildCount: categories.length, - children: categories - .map( - (category) => GestureDetector( - child: CategoryCard(category), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return RecipesListScreen(category: category.name); - }, - ), + children: [ + for (int i = 0; i < categories.length; i++) + GestureDetector( + child: CategoryCard( + categories.elementAt(i), + recipe?.elementAt(i)?.recipeId, + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return RecipesListScreen( + category: categories.elementAt(i).name, + ); + }, ), ), - ) - .toList(), + ), + ], ), ); } diff --git a/lib/src/screens/form/recipe_form.dart b/lib/src/screens/form/recipe_form.dart index 1ad7576c..f401f522 100644 --- a/lib/src/screens/form/recipe_form.dart +++ b/lib/src/screens/form/recipe_form.dart @@ -3,8 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/input/duration_form_field.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/input/integer_text_form_field.dart'; @@ -24,15 +24,15 @@ class RecipeForm extends StatefulWidget { class _RecipeFormState extends State { final _formKey = GlobalKey(); - late MutableRecipe _mutableRecipe; + late RecipeBuilder _mutableRecipe; late TextEditingController categoryController; @override void initState() { - _mutableRecipe = Recipe.empty().toMutableRecipe(); + _mutableRecipe = RecipeBuilder(); if (widget.recipe != null) { - _mutableRecipe = widget.recipe!.toMutableRecipe(); + _mutableRecipe.replace(widget.recipe!); } categoryController = TextEditingController(text: _mutableRecipe.recipeCategory); @@ -110,7 +110,7 @@ class _RecipeFormState extends State { ), suggestionsCallback: DataRepository().getMatchingCategoryNames, - itemBuilder: (context, suggestion) { + itemBuilder: (context, String suggestion) { return ListTile( title: Text(suggestion), ); @@ -138,7 +138,7 @@ class _RecipeFormState extends State { TextFormField( enabled: enabled, initialValue: _mutableRecipe.keywords, - onChanged: (value) { + onSaved: (value) { _mutableRecipe.keywords = value; }, ), @@ -157,7 +157,7 @@ class _RecipeFormState extends State { TextFormField( enabled: enabled, initialValue: _mutableRecipe.url, - onChanged: (value) { + onSaved: (value) { _mutableRecipe.url = value; }, ), @@ -177,7 +177,7 @@ class _RecipeFormState extends State { enabled: false, style: const TextStyle(color: Colors.grey), initialValue: _mutableRecipe.imageUrl, - onChanged: (value) { + onSaved: (value) { _mutableRecipe.imageUrl = value; }, ), @@ -196,8 +196,6 @@ class _RecipeFormState extends State { IntegerTextFormField( enabled: enabled, initialValue: _mutableRecipe.recipeYield, - onChanged: (value) => - _mutableRecipe.recipeYield = value, onSaved: (value) => _mutableRecipe.recipeYield = value, ), ], @@ -247,11 +245,14 @@ class _RecipeFormState extends State { if (_formKey.currentState?.validate() ?? false) { _formKey.currentState?.save(); if (widget.recipe == null) { + _mutableRecipe.dateCreated = DateTime.now().toUtc(); BlocProvider.of(context) - .add(RecipeCreated(_mutableRecipe.toRecipe())); + .add(RecipeCreated(_mutableRecipe.build())); } else { + _mutableRecipe.dateModified = + DateTime.now().toUtc(); BlocProvider.of(context) - .add(RecipeUpdated(_mutableRecipe.toRecipe())); + .add(RecipeUpdated(_mutableRecipe.build())); } } }, diff --git a/lib/src/screens/recipe/recipe_screen.dart b/lib/src/screens/recipe/recipe_screen.dart index aa3d4ff9..3f983a5c 100644 --- a/lib/src/screens/recipe/recipe_screen.dart +++ b/lib/src/screens/recipe/recipe_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; @@ -13,8 +14,8 @@ import 'package:nextcloud_cookbook_flutter/src/screens/recipe_edit_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/animated_time_progress_bar.dart'; -import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/duration_indicator.dart'; +import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:wakelock/wakelock.dart'; @@ -124,7 +125,7 @@ class RecipeScreenState extends State { } FloatingActionButton _buildFabButton(Recipe recipe) { - final enabled = recipe.cookTime > Duration.zero; + final enabled = recipe.cookTime != null; return FloatingActionButton( onPressed: () { { @@ -164,12 +165,9 @@ class RecipeScreenState extends State { width: double.infinity, height: 200, child: Center( - child: AuthenticationCachedNetworkRecipeImage( - recipeId: recipe.id, - full: true, - width: double.infinity, - height: 200, - boxFit: BoxFit.cover, + child: RecipeImage( + id: recipe.id, + size: const Size(double.infinity, 200), ), ), ), @@ -239,19 +237,19 @@ class RecipeScreenState extends State { runSpacing: 10, spacing: 10, children: [ - if (recipe.prepTime > Duration.zero) + if (recipe.prepTime != null) DurationIndicator( - duration: recipe.prepTime, + duration: recipe.prepTime!, name: translate('recipe.prep'), ), - if (recipe.cookTime > Duration.zero) + if (recipe.cookTime != null) DurationIndicator( - duration: recipe.cookTime, + duration: recipe.cookTime!, name: translate('recipe.cook'), ), - if (recipe.totalTime > Duration.zero) + if (recipe.totalTime != null) DurationIndicator( - duration: recipe.totalTime, + duration: recipe.totalTime!, name: translate('recipe.total'), ), ], @@ -284,7 +282,9 @@ class RecipeScreenState extends State { ), ), ), - if (isLargeScreen && recipe.recipeIngredient.isNotEmpty) + if (isLargeScreen && + recipe.recipeIngredient.isNotEmpty && + recipe.nutritionList.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 10.0), child: Row( @@ -292,7 +292,7 @@ class RecipeScreenState extends State { children: [ Expanded( flex: 5, - child: NutritionList(recipe.nutrition), + child: NutritionList(recipe.nutritionList), ), Expanded( flex: 5, @@ -316,8 +316,8 @@ class RecipeScreenState extends State { padding: const EdgeInsets.only(bottom: 10.0), child: Column( children: [ - if (recipe.nutrition.isNotEmpty) - NutritionList(recipe.nutrition), + if (recipe.nutritionList.isNotEmpty) + NutritionList(recipe.nutritionList), if (recipe.recipeIngredient.isNotEmpty) IngredientList(recipe, settingsBasedTextStyle), InstructionList(recipe, settingsBasedTextStyle) diff --git a/lib/src/screens/recipe/widget/ingredient_list.dart b/lib/src/screens/recipe/widget/ingredient_list.dart index f5542edc..bc86a7b0 100644 --- a/lib/src/screens/recipe/widget/ingredient_list.dart +++ b/lib/src/screens/recipe/widget/ingredient_list.dart @@ -1,7 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; class IngredientList extends StatefulWidget { final Recipe _recipe; diff --git a/lib/src/screens/recipe/widget/instruction_list.dart b/lib/src/screens/recipe/widget/instruction_list.dart index 49a806fa..1bed4ee2 100644 --- a/lib/src/screens/recipe/widget/instruction_list.dart +++ b/lib/src/screens/recipe/widget/instruction_list.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; class InstructionList extends StatefulWidget { final Recipe _recipe; diff --git a/lib/src/screens/recipe_edit_screen.dart b/lib/src/screens/recipe_edit_screen.dart index b6159011..96f130b8 100644 --- a/lib/src/screens/recipe_edit_screen.dart +++ b/lib/src/screens/recipe_edit_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/form/recipe_form.dart'; class RecipeEditScreen extends StatelessWidget { @@ -19,7 +19,7 @@ class RecipeEditScreen extends StatelessWidget { onWillPop: () { final RecipeBloc recipeBloc = BlocProvider.of(context); if (recipeBloc.state.status == RecipeStatus.updateFailure) { - recipeBloc.add(RecipeLoaded(recipe.id)); + recipeBloc.add(RecipeLoaded(recipe.id!)); } return Future(() => true); }, diff --git a/lib/src/screens/recipe_import_screen.dart b/lib/src/screens/recipe_import_screen.dart index ef7f73d2..6e7163e2 100644 --- a/lib/src/screens/recipe_import_screen.dart +++ b/lib/src/screens/recipe_import_screen.dart @@ -36,7 +36,7 @@ class RecipeImportScreen extends StatelessWidget { context, MaterialPageRoute( builder: (context) { - return RecipeScreen(recipeId: state.recipeId!); + return RecipeScreen(recipeId: state.recipe!.id!); }, ), ); diff --git a/lib/src/screens/recipes_list_screen.dart b/lib/src/screens/recipes_list_screen.dart index fc2d47c3..817c7334 100644 --- a/lib/src/screens/recipes_list_screen.dart +++ b/lib/src/screens/recipes_list_screen.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/blocs/recipes_short/recipes_short_bloc.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe/recipe_screen.dart'; -import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; +import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; class RecipesListScreen extends StatefulWidget { final String category; @@ -95,11 +95,9 @@ class RecipesListScreenState extends State { ListTile _buildRecipeStubScreen(RecipeStub recipe) { return ListTile( title: Text(recipe.name), - trailing: AuthenticationCachedNetworkRecipeImage( - recipeId: recipe.recipeId, - full: false, - width: 60, - height: 60, + trailing: RecipeImage( + id: recipe.recipeId, + size: const Size.square(60), ), onTap: () { Navigator.push( diff --git a/lib/src/screens/timer_screen.dart b/lib/src/screens/timer_screen.dart index 9d69e222..0adc7ee7 100644 --- a/lib/src/screens/timer_screen.dart +++ b/lib/src/screens/timer_screen.dart @@ -4,7 +4,7 @@ import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe/recipe_screen.dart'; import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; import 'package:nextcloud_cookbook_flutter/src/widget/animated_time_progress_bar.dart'; -import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; +import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; class TimerScreen extends StatefulWidget { const TimerScreen({super.key}); @@ -66,11 +66,9 @@ class _TimerScreen extends State { ListTile _buildListItem(Timer timer) { return ListTile( key: UniqueKey(), - leading: AuthenticationCachedNetworkRecipeImage( - recipeId: timer.recipeId, - full: false, - width: 60, - height: 60, + leading: RecipeImage( + id: timer.recipeId, + size: const Size.square(60), ), title: Text(timer.title), subtitle: AnimatedTimeProgressBar( diff --git a/lib/src/services/api_provider.dart b/lib/src/services/api_provider.dart new file mode 100644 index 00000000..1083949c --- /dev/null +++ b/lib/src/services/api_provider.dart @@ -0,0 +1,30 @@ +part of 'services.dart'; + +class ApiProvider { + static final ApiProvider _apiProvider = ApiProvider._(); + + factory ApiProvider() => _apiProvider; + ApiProvider._() { + final auth = UserRepository().currentAppAuthentication; + + ncCookbookApi = NcCookbookApi( + basePathOverride: '${auth.server}/apps/cookbook', + ); + + ncCookbookApi.setBasicAuth( + "app_password", + auth.loginName, + auth.password, + ); + recipeApi = ncCookbookApi.getRecipesApi(); + categoryApi = ncCookbookApi.getCategoriesApi(); + miscApi = ncCookbookApi.getMiscApi(); + tagsApi = ncCookbookApi.getTagsApi(); + } + + late NcCookbookApi ncCookbookApi; + late RecipesApi recipeApi; + late CategoriesApi categoryApi; + late MiscApi miscApi; + late TagsApi tagsApi; +} diff --git a/lib/src/services/categories_provider.dart b/lib/src/services/categories_provider.dart deleted file mode 100644 index 088212fe..00000000 --- a/lib/src/services/categories_provider.dart +++ /dev/null @@ -1,32 +0,0 @@ -part of 'services.dart'; - -class CategoriesProvider { - Future> fetchCategories() async { - final AppAuthentication appAuthentication = - UserRepository().currentAppAuthentication; - - final String url = - "${appAuthentication.server}/index.php/apps/cookbook/api/v1/categories"; - - // Parse categories - try { - 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, - (int previousValue, Category element) => - previousValue + element.recipeCount, - ), - ), - ); - return categories; - } catch (e) { - throw Exception(e); - } - } -} diff --git a/lib/src/services/category_recipes_short_provider.dart b/lib/src/services/category_recipes_short_provider.dart deleted file mode 100644 index 51f4f585..00000000 --- a/lib/src/services/category_recipes_short_provider.dart +++ /dev/null @@ -1,29 +0,0 @@ -part of 'services.dart'; - -class CategoryRecipesShortProvider { - Future> fetchCategoryRecipesShort(String category) async { - final AndroidApiVersion androidApiVersion = - UserRepository().getAndroidVersion(); - - final AppAuthentication appAuthentication = - UserRepository().currentAppAuthentication; - - String url = - "${appAuthentication.server}/apps/cookbook/api/v1/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/$categorySanitized"; - } - - // Parse categories - try { - final String contents = await Network().get(url); - return RecipeStub.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 deleted file mode 100644 index 9742188b..00000000 --- a/lib/src/services/category_search_provider.dart +++ /dev/null @@ -1,19 +0,0 @@ -part of 'services.dart'; - -class CategorySearchProvider { - List categoryNames = []; - bool categoriesLoaded = false; - static String categoryAll = translate('categories.all_categories'); - - void updateCategoryNames(List categories) { - categoryNames = categories - .map((e) => e.name) - .where((element) => element != categoryAll && element != '*') - .toList(); - categoriesLoaded = true; - } - - Iterable getMatchingCategoryNames(String pattern) { - return categoryNames.where((element) => element.contains(pattern)); - } -} diff --git a/lib/src/services/data_repository.dart b/lib/src/services/data_repository.dart index 2639b942..bed178d7 100644 --- a/lib/src/services/data_repository.dart +++ b/lib/src/services/data_repository.dart @@ -8,79 +8,97 @@ class DataRepository { DataRepository._(); // Provider List - RecipesShortProvider recipesShortProvider = RecipesShortProvider(); - CategoryRecipesShortProvider categoryRecipesShortProvider = - CategoryRecipesShortProvider(); - CategorySearchProvider categorySearchProvider = CategorySearchProvider(); - RecipeProvider recipeProvider = RecipeProvider(); - CategoriesProvider categoriesProvider = CategoriesProvider(); + final ApiProvider api = ApiProvider(); + final NextcloudMetadataApi _nextcloudMetadataApi = NextcloudMetadataApi(); // Data - static String categoryAll = translate('categories.all_categories'); + static final String categoryAll = translate('categories.all_categories'); // Actions - Future> fetchRecipesShort({required String category}) { + Future?> fetchRecipesShort({ + required String category, + }) async { if (category == categoryAll) { - return recipesShortProvider.fetchRecipesShort(); + final response = await api.recipeApi.listRecipes(); + return response.data; + } else if (category == "*") { + final response = await api.categoryApi.recipesInCategory(category: "_"); + return response.data; } else { - return categoryRecipesShortProvider.fetchCategoryRecipesShort(category); + final response = + await api.categoryApi.recipesInCategory(category: category); + return response.data; } } - Future fetchRecipe(String id) { - return recipeProvider.fetchRecipe(int.parse(id)); + Future fetchRecipe(String id) async { + final response = await api.recipeApi.recipeDetails(id: id); + return response.data; } - Future updateRecipe(Recipe recipe) async { - final response = await recipeProvider.updateRecipe(recipe); - return response.toString(); + Future updateRecipe(Recipe recipe) async { + final response = + await api.recipeApi.updateRecipe(id: recipe.id!, recipe: recipe); + return response.data?.toString(); } - Future createRecipe(Recipe recipe) async { - final response = await recipeProvider.createRecipe(recipe); - - return response.toString(); + Future createRecipe(Recipe recipe) async { + final response = await api.recipeApi.newRecipe(recipe: recipe); + return response.data?.toString(); } - Future importRecipe(String url) { - return recipeProvider.importRecipe(url); + Future importRecipe(String url) async { + final requestUrl = UrlBuilder()..url = url; + + final response = await api.recipeApi.callImport(url: requestUrl.build()); + return response.data; } - Future> fetchCategories() { - return categoriesProvider.fetchCategories(); + Future?> fetchCategories() async { + final response = await api.categoryApi.listCategories(); + final categories = response.data?.toBuilder(); + + final allRecepies = await fetchAllRecipes(); + final allCount = allRecepies?.length; + + if (allCount != null && allCount > 0) { + final allCategory = CategoryBuilder() + ..name = categoryAll + ..recipeCount = allCount; + + categories?.insert( + 0, + allCategory.build(), + ); + } + + return categories?.build(); } - Future> fetchCategoryMainRecipes( - List categories, + Future?> fetchCategoryMainRecipes( + Iterable? categories, ) async { - return Future.wait( - categories.map((category) => _fetchCategoryMainRecipe(category)).toList(), - ); - } + if (categories == null) return null; - Future _fetchCategoryMainRecipe(Category category) async { - List categoryRecipes = []; + return Future.wait(categories.map(_fetchCategoryMainRecipe)); + } + Future _fetchCategoryMainRecipe(Category category) async { try { - if (category.name == translate('categories.all_categories')) { - categoryRecipes = await recipesShortProvider.fetchRecipesShort(); - } else { - categoryRecipes = await categoryRecipesShortProvider - .fetchCategoryRecipesShort(category.name); + final categoryRecipes = await fetchRecipesShort(category: category.name); + if (categoryRecipes != null && categoryRecipes.isNotEmpty) { + return categoryRecipes.first; } } catch (e) { log("Could not load main recipe of Category!"); + rethrow; } - if (categoryRecipes.isNotEmpty) { - return category.copyWith(firstRecipeId: categoryRecipes.first.recipeId); - } - - return category; + return null; } - Future> fetchAllRecipes() async { + Future?> fetchAllRecipes() async { return fetchRecipesShort(category: categoryAll); } @@ -88,15 +106,38 @@ class DataRepository { return _nextcloudMetadataApi.getUserAvatarUrl(); } - void updateCategoryNames(List categories) { - categorySearchProvider.updateCategoryNames(categories); + Future> getMatchingCategoryNames(String pattern) async { + final categories = await fetchCategories(); + final matches = + categories?.where((element) => element.name.contains(pattern)); + + return matches?.map((e) => e.name) ?? []; } - Future> getMatchingCategoryNames(String pattern) async { - if (!categorySearchProvider.categoriesLoaded) { - await categoriesProvider.fetchCategories(); + Future fetchImage(String recipeId, Size size) async { + final String sizeParam; + if (size.longestSide <= 16) { + sizeParam = "thumb16"; + } else if (size.longestSide <= 250) { + sizeParam = "thumb"; + } else { + sizeParam = "full"; + } + + final response = await api.recipeApi.getImage( + id: recipeId, + headers: { + "Accept": "image/jpeg, image/svg+xml", + }, + size: sizeParam, + ); + if (response.data != null) { + return ImageResponse( + data: response.data!, + isSvg: response.headers.value("content-type") == "image/svg+xml", + ); } - return categorySearchProvider.getMatchingCategoryNames(pattern); + return null; } } diff --git a/lib/src/services/network.dart b/lib/src/services/network.dart deleted file mode 100644 index ee8fd6eb..00000000 --- a/lib/src/services/network.dart +++ /dev/null @@ -1,27 +0,0 @@ -part of 'services.dart'; - -class Network { - static final Network _network = Network._(); - - factory Network() => _network; - - Network._(); - - /// Try to load file from locale cache first, if not available get it from the server - Future get(String url) async { - 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, - }, - ); - - final String contents = await file.file.readAsString(); - return contents; - } -} diff --git a/lib/src/services/recipe_provider.dart b/lib/src/services/recipe_provider.dart deleted file mode 100644 index d4155db2..00000000 --- a/lib/src/services/recipe_provider.dart +++ /dev/null @@ -1,82 +0,0 @@ -part of 'services.dart'; - -class RecipeProvider { - Future fetchRecipe(int id) async { - final AppAuthentication appAuthentication = - UserRepository().currentAppAuthentication; - - final String url = - "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes/$id"; - // Parse categories - try { - final String contents = await Network().get(url); - return Recipe.fromJsonString(contents); - } catch (e) { - throw Exception(e); - } - } - - Future updateRecipe(Recipe recipe) async { - 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}"; - final response = await client.put( - url, - data: recipe.toJsonString(), - options: Options( - contentType: "application/json;charset=UTF-8", - ), - ); - // Refresh recipe in the cache - await DefaultCacheManager().removeFile(url); - return int.parse(response.data as String); - } catch (e) { - throw Exception(e); - } - } - - Future createRecipe(Recipe recipe) async { - final Dio client = UserRepository().authenticatedClient; - final AppAuthentication appAuthentication = - UserRepository().currentAppAuthentication; - - try { - final response = await client.post( - "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes", - data: recipe.toJsonString(), - 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 { - final Dio client = UserRepository().authenticatedClient; - final AppAuthentication appAuthentication = - UserRepository().currentAppAuthentication; - - try { - 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.fromJsonString(response.data as String); - } on DioError catch (e) { - throw Exception(e.response); - } catch (e) { - throw Exception(e); - } - } -} diff --git a/lib/src/services/recipes_short_provider.dart b/lib/src/services/recipes_short_provider.dart deleted file mode 100644 index cea633da..00000000 --- a/lib/src/services/recipes_short_provider.dart +++ /dev/null @@ -1,17 +0,0 @@ -part of 'services.dart'; - -class RecipesShortProvider { - Future> fetchRecipesShort() async { - final AppAuthentication appAuthentication = - UserRepository().currentAppAuthentication; - - final String url = - "${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes"; - try { - final String contents = await Network().get(url); - return RecipeStub.parseRecipesShort(contents); - } catch (e) { - throw Exception(translate('recipe_list.errors.load_failed')); - } - } -} diff --git a/lib/src/services/services.dart b/lib/src/services/services.dart index 50cf5a17..af979bdc 100644 --- a/lib/src/services/services.dart +++ b/lib/src/services/services.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:convert'; import 'dart:developer'; @@ -7,34 +6,25 @@ import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/recipe_short.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/image_response.dart'; import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; import 'package:nextcloud_cookbook_flutter/src/screens/recipe_import_screen.dart'; -import 'package:nextcloud_cookbook_flutter/src/util/custom_cache_manager.dart'; import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart'; import 'package:timezone/data/latest_10y.dart' as tz; import 'package:timezone/timezone.dart' as tz; import 'package:xml/xml.dart'; +part "api_provider.dart"; part "authentication_provider.dart"; -part "categories_provider.dart"; -part "category_recipes_short_provider.dart"; -part "category_search_provider.dart"; part "data_repository.dart"; part "intent_repository.dart"; part "net/nextcloud_metadata_api.dart"; -part "network.dart"; part "notification_provider.dart"; -part "recipe_provider.dart"; -part "recipes_short_provider.dart"; part "user_repository.dart"; -part "version_provider.dart"; part 'timer_repository.dart'; diff --git a/lib/src/services/user_repository.dart b/lib/src/services/user_repository.dart index 11bb3621..95b96802 100644 --- a/lib/src/services/user_repository.dart +++ b/lib/src/services/user_repository.dart @@ -8,7 +8,6 @@ class UserRepository { UserRepository._(); AuthenticationProvider authenticationProvider = AuthenticationProvider(); - VersionProvider versionProvider = VersionProvider(); Future authenticate( String serverUrl, @@ -76,11 +75,13 @@ class UserRepository { return authenticationProvider.deleteAppAuthentication(); } - Future fetchApiVersion() async { - return versionProvider.fetchApiVersion(); + bool isVersionSupported(APIVersion version) { + return ApiProvider().ncCookbookApi.isSupportedSync(version); } - AndroidApiVersion getAndroidVersion() { - return versionProvider.getApiVersion().getAndroidVersion(); + Future fetchApiVersion() async { + final response = await ApiProvider().miscApi.version(); + + return response.data!.apiVersion; } } diff --git a/lib/src/services/version_provider.dart b/lib/src/services/version_provider.dart deleted file mode 100644 index 8ae74448..00000000 --- a/lib/src/services/version_provider.dart +++ /dev/null @@ -1,108 +0,0 @@ -part of 'services.dart'; - -class VersionProvider { - late ApiVersion _currentApiVersion; - bool warningWasShown = false; - - Future fetchApiVersion() async { - warningWasShown = false; - - final AppAuthentication appAuthentication = - UserRepository().currentAppAuthentication; - - 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.fromJson(response.data.toString()); - } catch (e) { - _currentApiVersion = ApiVersion(0, 0, 0, 0, 0); - _currentApiVersion.loadFailureMessage = e.toString(); - } - } else { - _currentApiVersion = ApiVersion(0, 0, 0, 0, 0); - } - - return _currentApiVersion; - } - - ApiVersion getApiVersion() { - return _currentApiVersion; - } -} - -class ApiVersion { - static const int confirmedMajorAPIVersion = 1; - static const int confirmedMinorAPIVersion = 1; - - final int majorApiVersion; - final int minorApiVersion; - final int majorAppVersion; - final int minorAppVersion; - final int patchAppVersion; - - String loadFailureMessage = ""; - - ApiVersion( - this.majorApiVersion, - this.minorApiVersion, - this.majorAppVersion, - this.minorAppVersion, - this.patchAppVersion, - ); - - 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"); - } - - final appVersion = (data["cookbook_version"] as List).cast(); - final apiVersion = data["api_version"] as Map; - - if (!(appVersion.length == 3 && - apiVersion.containsKey("major") && - apiVersion.containsKey("minor"))) { - throw Exception("Required Fields not present!\n$jsonString"); - } - - return ApiVersion( - apiVersion["major"] as int, - apiVersion["minor"] as int, - appVersion[0], - appVersion[1], - appVersion[2], - ); - } - - /// Returns a VersionCode that indicates the app which endpoints to call. - /// Versions only need to be adapted if backwards comparability is required. - AndroidApiVersion getAndroidVersion() { - if (majorApiVersion == 0 && minorApiVersion == 0) { - return AndroidApiVersion.beforeApiEndpoint; - } else { - return AndroidApiVersion.categoryApiTransition; - } - } - - bool isVersionAboveConfirmed() { - if (majorApiVersion > confirmedMajorAPIVersion || - (majorApiVersion == confirmedMajorAPIVersion && - minorApiVersion > confirmedMinorAPIVersion)) { - return true; - } else { - return false; - } - } - - @override - String toString() { - return "ApiVersion: $majorApiVersion.$minorApiVersion AppVersion: $majorAppVersion.$minorAppVersion.$patchAppVersion"; - } -} - -enum AndroidApiVersion { beforeApiEndpoint, categoryApiTransition } diff --git a/lib/src/util/custom_cache_manager.dart b/lib/src/util/custom_cache_manager.dart deleted file mode 100644 index 175314c5..00000000 --- a/lib/src/util/custom_cache_manager.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:http/io_client.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; - -class CustomCacheManager { - static final CustomCacheManager _instance = CustomCacheManager._(); - - factory CustomCacheManager() => _instance; - - CustomCacheManager._(); - - static const key = 'customCacheKey'; - final UserRepository _userRepository = UserRepository(); - - final CacheManager _selfSignedCacheManager = CacheManager( - Config( - key, - fileService: HttpFileService( - httpClient: IOClient( - HttpClient() - ..badCertificateCallback = - (X509Certificate cert, String host, int port) => true, - ), - ), - ), - ); - - 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 deleted file mode 100644 index 343e15a4..00000000 --- a/lib/src/util/iso_time_format.dart +++ /dev/null @@ -1,39 +0,0 @@ -// 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)) { - throw ArgumentError("String does not follow correct format"); - } - - final weeks = _parseTime(isoString, "W"); - final days = _parseTime(isoString, "D"); - final hours = _parseTime(isoString, "H"); - final minutes = _parseTime(isoString, "M"); - final seconds = _parseTime(isoString, "S"); - - return Duration( - days: days + (weeks * 7), - hours: hours, - minutes: minutes, - seconds: seconds, - ); - } - - /// Private helper method for extracting a time value from the ISO8601 string. - static int _parseTime(String duration, String timeUnit) { - final timeMatch = RegExp(r"\d+" + timeUnit).firstMatch(duration); - - final timeString = timeMatch?.group(0); - - if (timeString == null) { - return 0; - } - - return int.parse(timeString.substring(0, timeString.length - 1)); - } -} diff --git a/lib/src/util/nutrition_utilty.dart b/lib/src/util/nutrition_utilty.dart deleted file mode 100644 index 70f0e2ea..00000000 --- a/lib/src/util/nutrition_utilty.dart +++ /dev/null @@ -1,18 +0,0 @@ -// ignore_for_file: avoid_classes_with_only_static_members - -class NutritionUtility { - static final nutritionProperties = [ - "calories", - "carbohydrateContent", - "cholesterolContent", - "fatContent", - "fiberContent", - "proteinContent", - "saturatedFatContent", - "servingSize", - "sodiumContent", - "sugarContent", - "transFatContent", - "unsaturatedFatContent" - ]; -} diff --git a/lib/src/util/self_signed_certificate_http_overrides.dart b/lib/src/util/self_signed_certificate_http_overrides.dart deleted file mode 100644 index 98704878..00000000 --- a/lib/src/util/self_signed_certificate_http_overrides.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'dart:io'; - -class SelfSignedCertificateHttpOverride extends HttpOverrides { - @override - HttpClient createHttpClient(SecurityContext? context) { - return super.createHttpClient(context) - ..badCertificateCallback = - (X509Certificate cert, String host, int port) => true; - } -} diff --git a/lib/src/widget/api_version_warning.dart b/lib/src/widget/api_version_warning.dart deleted file mode 100644 index 5039ac79..00000000 --- a/lib/src/widget/api_version_warning.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; - -class ApiVersionWarning extends StatelessWidget { - const ApiVersionWarning({super.key}); - - @override - Widget build(BuildContext context) { - final VersionProvider versionProvider = UserRepository().versionProvider; - final ApiVersion apiVersion = versionProvider.getApiVersion(); - - if (!versionProvider.warningWasShown) { - versionProvider.warningWasShown = true; - Future.delayed(const Duration(milliseconds: 100), () { - if (apiVersion.loadFailureMessage.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - translate( - "categories.errors.api_version_check_failed", - args: {"error_msg": apiVersion.loadFailureMessage}, - ), - ), - backgroundColor: Colors.red, - ), - ); - } else if (apiVersion.isVersionAboveConfirmed()) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - translate( - "categories.errors.api_version_above_confirmed", - args: { - "version": - "${apiVersion.majorApiVersion}.${apiVersion.minorApiVersion}" - }, - ), - ), - backgroundColor: Colors.orange, - ), - ); - } - }); - } - return Container(); - } -} diff --git a/lib/src/widget/authentication_cached_network_image.dart b/lib/src/widget/authentication_cached_network_image.dart deleted file mode 100644 index e5806a12..00000000 --- a/lib/src/widget/authentication_cached_network_image.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; - -class AuthenticationCachedNetworkImage extends StatelessWidget { - final double? width; - final double? height; - final BoxFit? boxFit; - - final String url; - - final Widget? errorWidget; - - const AuthenticationCachedNetworkImage({ - super.key, - required this.url, - this.width, - this.height, - this.boxFit, - this.errorWidget, - }); - - @override - Widget build(BuildContext context) { - final AppAuthentication appAuthentication = - UserRepository().currentAppAuthentication; - - return CachedNetworkImage( - fit: boxFit, - width: width, - height: height, - httpHeaders: { - "Authorization": appAuthentication.basicAuth, - "Accept": "image/jpeg" - }, - imageUrl: url, - placeholder: (context, url) => const Center( - child: CircularProgressIndicator(), - ), - errorWidget: (context, url, error) => Container( - width: width, - height: height, - color: Colors.grey[400], - child: errorWidget, - ), - ); - } -} diff --git a/lib/src/widget/authentication_cached_network_recipe_image.dart b/lib/src/widget/authentication_cached_network_recipe_image.dart deleted file mode 100644 index d721743a..00000000 --- a/lib/src/widget/authentication_cached_network_recipe_image.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart'; -import 'package:nextcloud_cookbook_flutter/src/services/services.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 String recipeId; - final bool full; - - const AuthenticationCachedNetworkRecipeImage({ - super.key, - required this.recipeId, - required this.full, - this.width, - this.height, - this.boxFit, - }); - - @override - Widget build(BuildContext context) { - final AppAuthentication appAuthentication = - UserRepository().currentAppAuthentication; - - final String settings = full ? "full" : "thumb"; - - return AuthenticationCachedNetworkImage( - url: - '${appAuthentication.server}/index.php/apps/cookbook/api/v1/recipes/$recipeId/image?size=$settings', - width: width, - height: height, - boxFit: boxFit, - errorWidget: SvgPicture.asset( - 'assets/icon.svg', - ), - ); - } -} diff --git a/lib/src/widget/category_card.dart b/lib/src/widget/category_card.dart index 31156be7..3777a023 100644 --- a/lib/src/widget/category_card.dart +++ b/lib/src/widget/category_card.dart @@ -1,14 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; -import 'package:nextcloud_cookbook_flutter/src/models/category.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/util/setting_keys.dart'; -import 'package:nextcloud_cookbook_flutter/src/widget/authentication_cached_network_recipe_image.dart'; +import 'package:nextcloud_cookbook_flutter/src/widget/recipe_image.dart'; class CategoryCard extends StatelessWidget { final Category category; + final String? imageID; const CategoryCard( - this.category, { + this.category, + this.imageID, { super.key, }); @@ -29,24 +31,13 @@ class CategoryCard extends StatelessWidget { colors: [Colors.black, Colors.transparent], ).createShader(bounds); }, - child: category.firstRecipeId != null - ? ClipRRect( - borderRadius: BorderRadius.circular(5), - child: AuthenticationCachedNetworkRecipeImage( - recipeId: category.firstRecipeId!, - full: false, - boxFit: BoxFit.cover, - ), - ) - : ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Container( - color: Colors.grey[400], - child: const Center( - child: CircularProgressIndicator(), - ), - ), - ), + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: RecipeImage( + id: imageID, + size: const Size.square(250), + ), + ), ), Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/src/widget/input/duration_form_field.dart b/lib/src/widget/input/duration_form_field.dart index b6d54616..6831cbf7 100644 --- a/lib/src/widget/input/duration_form_field.dart +++ b/lib/src/widget/input/duration_form_field.dart @@ -6,8 +6,8 @@ import 'package:nextcloud_cookbook_flutter/src/widget/input/integer_text_form_fi class DurationFormField extends StatefulWidget { final String title; final RecipeState state; - final Duration duration; - final void Function(Duration value) onChanged; + final Duration? duration; + final void Function(Duration? value) onChanged; const DurationFormField({ super.key, @@ -22,9 +22,11 @@ class DurationFormField extends StatefulWidget { } class _DurationFormFieldState extends State { - late Duration currentDuration; + late Duration? currentDuration; late bool enabled; + Duration get duaration => currentDuration ?? Duration.zero; + @override void initState() { currentDuration = widget.duration; @@ -54,13 +56,12 @@ class _DurationFormFieldState extends State { width: 70, child: IntegerTextFormField( enabled: enabled, - initialValue: widget.duration.inHours, + initialValue: duaration.inHours, decoration: InputDecoration( hintText: translate('recipe.fields.time.hours'), ), onChanged: (value) { currentDuration = _updateDuration( - currentDuration: currentDuration, hours: value, ); widget.onChanged(currentDuration); @@ -75,14 +76,13 @@ class _DurationFormFieldState extends State { width: 50, child: IntegerTextFormField( enabled: enabled, - initialValue: widget.duration.inMinutes % 60, + initialValue: duaration.inMinutes % 60, maxValue: 60, decoration: InputDecoration( hintText: translate('recipe.fields.time.minutes'), ), onChanged: (value) { currentDuration = _updateDuration( - currentDuration: currentDuration, minutes: value, ); widget.onChanged(currentDuration); @@ -95,19 +95,18 @@ class _DurationFormFieldState extends State { ); } - Duration _updateDuration({ - required Duration currentDuration, + Duration? _updateDuration({ int? hours, int? minutes, }) { if (hours != null) { - final int currentMinutes = currentDuration.inMinutes % 60; + final int currentMinutes = duaration.inMinutes % 60; return Duration(hours: hours, minutes: currentMinutes); } if (minutes != null) { - final int currentHours = currentDuration.inHours; + final int currentHours = duaration.inHours; return Duration(hours: currentHours, minutes: minutes); } diff --git a/lib/src/widget/input/integer_text_form_field.dart b/lib/src/widget/input/integer_text_form_field.dart index 0a9f552e..2788fcfd 100644 --- a/lib/src/widget/input/integer_text_form_field.dart +++ b/lib/src/widget/input/integer_text_form_field.dart @@ -11,14 +11,15 @@ class IntegerTextFormField extends StatefulWidget { const IntegerTextFormField({ super.key, - this.initialValue = 0, + int? initialValue, this.enabled, this.decoration, this.onChanged, this.onSaved, this.minValue, this.maxValue, - }) : assert((minValue == null || maxValue == null) || minValue <= maxValue); + }) : initialValue = initialValue ?? 0, + assert((minValue == null || maxValue == null) || minValue <= maxValue); @override State createState() => _IntegerTextFormFieldState(); diff --git a/lib/src/widget/input/reorderable_list_form_field.dart b/lib/src/widget/input/reorderable_list_form_field.dart index b0855662..8033b0a6 100644 --- a/lib/src/widget/input/reorderable_list_form_field.dart +++ b/lib/src/widget/input/reorderable_list_form_field.dart @@ -1,12 +1,13 @@ +import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_reorderable_list/flutter_reorderable_list.dart' as rl; import 'package:nextcloud_cookbook_flutter/src/blocs/recipe/recipe_bloc.dart'; class ReorderableListFormField extends StatefulWidget { final String title; - final List items; + final ListBuilder items; final RecipeState state; - final Function(List value) onSave; + final Function(ListBuilder value) onSave; const ReorderableListFormField({ super.key, @@ -88,7 +89,7 @@ class _ReorderableListFormFieldState extends State { initialValue: "", enabled: false, onSaved: (_) { - widget.onSave(_items.map((e) => e.text).toList()); + widget.onSave(ListBuilder(_items.map((e) => e.text))); }, ), ), diff --git a/lib/src/widget/recipe_image.dart b/lib/src/widget/recipe_image.dart new file mode 100644 index 00000000..fa16c4f3 --- /dev/null +++ b/lib/src/widget/recipe_image.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/image_response.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; + +class RecipeImage extends StatelessWidget { + final Size size; + + final String? id; + + const RecipeImage({ + super.key, + required this.size, + required this.id, + }); + + @override + Widget build(BuildContext context) { + const boxFit = BoxFit.cover; + final color = Colors.grey[400]!; + + return SizedBox.fromSize( + size: size, + child: FutureBuilder( + future: id != null ? DataRepository().fetchImage(id!, size) : null, + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + if (snapshot.data!.isSvg) { + return ColoredBox( + color: color, + child: SvgPicture.memory( + snapshot.data!.data, + fit: boxFit, + ), + ); + } else { + return Image.memory( + snapshot.data!.data, + fit: boxFit, + ); + } + } + + if (snapshot.connectionState == ConnectionState.done) { + return ColoredBox( + color: color, + child: SvgPicture.asset( + 'assets/icon.svg', + fit: boxFit, + ), + ); + } + + return ColoredBox( + color: color, + child: const Center( + child: CircularProgressIndicator(), + ), + ); + }, + ), + ); + } +} diff --git a/lib/src/widget/user_image.dart b/lib/src/widget/user_image.dart new file mode 100644 index 00000000..a9f9942a --- /dev/null +++ b/lib/src/widget/user_image.dart @@ -0,0 +1,36 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; + +class UserImage extends StatelessWidget { + const UserImage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final url = DataRepository().getUserAvatarUrl(); + final appAuthentication = UserRepository().currentAppAuthentication; + + return ClipOval( + child: CachedNetworkImage( + cacheKey: "avatar", + fit: BoxFit.fill, + httpHeaders: { + "Authorization": appAuthentication.basicAuth, + "Accept": "image/jpeg" + }, + imageUrl: url, + placeholder: (context, url) => ColoredBox( + color: Colors.grey[400]!, + child: const Center( + child: CircularProgressIndicator(), + ), + ), + errorWidget: (context, url, error) => ColoredBox( + color: Colors.grey[400]!, + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d4160638..503f2469 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,13 +89,18 @@ dependencies: flutter_typeahead: ^4.3.7 - copy_with_extension: ^5.0.0 json_annotation: ^4.8.0 + nc_cookbook_api: + git: + url: https://github.com/Leptopoda/nextcloud_cookbook_dart_api.git + ref: 5465b2fd64fb71cb7cd588989d1709fd48884457 + + built_collection: ^5.1.1 + dev_dependencies: flutter_launcher_icons: ^0.12.0 - copy_with_extension_gen: ^5.0.0 json_serializable: ^6.6.0 build_runner: ^2.3.0