From ccb8d94be2221e1cac8d0e36a4464418ce03aec5 Mon Sep 17 00:00:00 2001 From: davidpkj Date: Sat, 30 Jul 2022 10:10:43 +0200 Subject: updated flutter, new recipe layout --- README.md | 32 ++++---- analysis_options.yaml | 2 +- lib/constants.dart | 20 +++-- lib/l10n/app_de.arb | 4 + lib/l10n/app_en.arb | 16 ++++ lib/l10n/app_ru.arb | 4 + lib/main.dart | 5 +- lib/models/data/recipe_data_class.dart | 1 - lib/views/image_view.dart | 28 +++---- lib/views/recipe_view.dart | 136 +++++++++++++++++++++++---------- lib/views/settings_view.dart | 5 +- lib/views/shoplist_view.dart | 2 +- lib/views/vote_view.dart | 3 +- lib/widgets/recipe_card_widget.dart | 4 +- pubspec.lock | 14 ++++ pubspec.yaml | 3 +- 16 files changed, 187 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index e4b4407..1702a82 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,21 @@ -# kulinar_app +# TODO -A new Flutter project. +## Before publish -## Details +- working import / export +- make recipe titles changeable across sessions +- compress / encode exports and internal images +- add settings explanations / subtitles +- change info links -### Dropdown +## Next update -- löschen: confirm screen -- einkaufsliste -- teilen -- publish +- average color background for recipe images +- add search based on ingridients, ie write noodle, get auflauf, noodle salad etc -### Headline +## Unsure -- edit -- burger - -### Onscreen - -- image -- stars -- favorite +- provide recipes over server(s), show source +- scheduling system +- voting system +- multiple images per recipe diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4d..1411db2 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,7 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +# include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the diff --git a/lib/constants.dart b/lib/constants.dart index 66d0257..4e570fd 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -const cVersion = "1.3.2"; +const cVersion = "1.4.0"; // Colors const cIconColor = Colors.white; @@ -33,12 +33,6 @@ const cInputHintStyle = TextStyle( fontSize: 14.0, ); -const cRecipeTextStyle = TextStyle( - color: Colors.black, - fontFamily: "Montserrat", - fontSize: 18.0, -); - const cDefaultTextStyle = TextStyle( fontFamily: "Montserrat", fontSize: 16.0, @@ -78,6 +72,18 @@ const cTinyTitleStyle = TextStyle( fontSize: 15.0, ); +const cRecipeTitleStyle = TextStyle( + color: Colors.black, + fontFamily: "Montserrat", + fontSize: 18.0, +); + +const cRecipeSubtitleStyle = TextStyle( + fontFamily: "RobotoSlab", + color: Colors.black54, + fontSize: 16.0, +); + const cRecipeDescriptionStyle = TextStyle( fontFamily: "RobotoSlab", color: Colors.black54, diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f566c6e..39bde48 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -45,6 +45,10 @@ "infoField2": "Version", "infoField3": "Größe", "infoField4": "Einträge", + "menu1": "Teilen", + "menu2": "Einkaufen", + "menu3": "Hochladen", + "menu4": "Löschen", "legalease": "Diese App unterliegt dem Urheberrecht des Entwicklers. Es gelten die Einschränkungen, Bedingungen und Berechtigungen der „BSD 2-Clause“ Lizenz. Lizenzen dritter sind unten beigefügt.", "noContentError": "Noch nichts zu sehen.", "noNetworkError": "Rezepte konnten nicht geladen werden.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 930595d..995c91a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -183,6 +183,22 @@ "@infoField4": { "description": "A file info text field" }, + "menu1": "Share", + "@menu1": { + "description": "The text for a recipe drop down menu" + }, + "menu2": "Shop", + "@menu2": { + "description": "The text for a recipe drop down menu" + }, + "menu3": "Upload", + "@menu3": { + "description": "The text for a recipe drop down menu" + }, + "menu4": "Delete", + "@menu4": { + "description": "The text for a recipe drop down menu" + }, "legalease": "This app is subject to the copyright of the developer. The limitations, conditions and permissions of the “BSD 2-Clause” license apply. Third-party licenses are enclosed below.", "@legalease": { "description": "Some legal stuff" diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 6b74dd7..4a7908c 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -45,6 +45,10 @@ "infoField2": "Версиа", "infoField3": "Размер", "infoField4": "Внесение", + "menu1": "Выслать", + "menu2": "Закупится", + "menu3": "Загрузить", + "menu4": "Удалить", "legalease": "Это приложение является объектом авторских прав разработчика. Применяются ограничения, условия и разрешения лицензии «BSD 2 Clause». Лицензии третьих личностей прилагаются ниже.", "noContentError": "Ешё пусто.", "noNetworkError": "Не удалось скачать рецепты.", diff --git a/lib/main.dart b/lib/main.dart index b76dc5b..d22d644 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,8 +12,6 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -// TODO: Should also probably compress / encode exports and internal images -// TODO: Add search based on ingridients, ie write noodle, get auflauf, noodle salad etc void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -63,8 +61,7 @@ class _AppRootState extends State { ThemeData themeData() { return ThemeData( - // TODO: Experiment what this does - // brightness: Brightness.light, + brightness: Brightness.light, primarySwatch: cPrimarySwatchColor, textSelectionTheme: TextSelectionThemeData( cursorColor: cIconColor, diff --git a/lib/models/data/recipe_data_class.dart b/lib/models/data/recipe_data_class.dart index ebbc5e1..fd4ef22 100644 --- a/lib/models/data/recipe_data_class.dart +++ b/lib/models/data/recipe_data_class.dart @@ -4,7 +4,6 @@ import 'package:kulinar_app/models/recipe_class.dart'; import 'package:kulinar_app/util/storage_handler.dart'; class RecipeData { - // TODO: What is this? static List remoteRecipeList = []; static List recipeList = []; diff --git a/lib/views/image_view.dart b/lib/views/image_view.dart index 8721d5e..615aa26 100644 --- a/lib/views/image_view.dart +++ b/lib/views/image_view.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:kulinar_app/constants.dart'; -// TODO: IMPLEMENT: zooming and multiple images possible class ImageView extends StatelessWidget { const ImageView({Key? key, required this.image}) : super(key: key); @@ -12,27 +11,28 @@ class ImageView extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: _averageImageColor(image), - body: Center( - child: Container( - child: GestureDetector( - child: Hero( - tag: "image", - child: Image( - image: FileImage(File(image)), + return InteractiveViewer( + child: Scaffold( + backgroundColor: _averageImageColor(image), + body: Center( + child: Container( + child: GestureDetector( + child: Hero( + tag: "image", + child: Image( + image: FileImage(File(image)), + ), ), + onTap: () { + Navigator.pop(context); + }, ), - onTap: () { - Navigator.pop(context); - }, ), ), ), ); } - // TODO: IMPLEMENT: average color background Color _averageImageColor(String path) { return cPrimaryColor; } diff --git a/lib/views/recipe_view.dart b/lib/views/recipe_view.dart index 960ba84..a0304c3 100644 --- a/lib/views/recipe_view.dart +++ b/lib/views/recipe_view.dart @@ -1,9 +1,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:http/http.dart'; import 'package:kulinar_app/constants.dart'; +import 'package:kulinar_app/models/data/shoplist_data_class.dart'; import 'package:kulinar_app/views/image_view.dart'; import 'package:kulinar_app/models/recipe_class.dart'; import 'package:kulinar_app/widgets/toastbar_widget.dart'; @@ -12,10 +12,13 @@ import 'package:kulinar_app/widgets/page_route_transitions.dart'; import 'package:kulinar_app/widgets/utility_icon_row_widget.dart'; import 'package:kulinar_app/models/data/settings_data_class.dart'; -import 'package:image_picker/image_picker.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:http/http.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class RecipeView extends StatefulWidget { const RecipeView({Key? key, this.remote = false, required this.readonly, this.recipe, this.redrawCallback, this.showToastCallback}) : super(key: key); @@ -108,7 +111,7 @@ class _RecipeViewState extends State { } } - void _removeRecipe() { + void _removeRecipe(BuildContext _) { Navigator.pop(context); RecipeData.recipeList.remove(widget.recipe); RecipeData.save(); @@ -118,11 +121,12 @@ class _RecipeViewState extends State { String _content = AppLocalizations.of(context)!.removed; String _actionLabel = AppLocalizations.of(context)!.undo; - // FIXME: This might throw due to "!" - widget.showToastCallback!(_content, _actionLabel, () { - _addRecipe(passedRecipe: _removedRecipe); - widget.redrawCallback!(); - }); + if (widget.showToastCallback != null) { + widget.showToastCallback!(_content, _actionLabel, () { + _addRecipe(passedRecipe: _removedRecipe); + widget.redrawCallback!(); + }); + } setState(() {}); } @@ -142,12 +146,10 @@ class _RecipeViewState extends State { setState(() {}); } - // FIXME: doesnt refresh, should be passed from parent void _pickImage() async { XFile? _pickedFile; if (SettingsData.settings["photoSource"] == "0") { - // TODO: Maybe pick multi image? _pickedFile = await ImagePicker().pickImage(source: ImageSource.camera); } else { _pickedFile = await ImagePicker().pickImage(source: ImageSource.gallery); @@ -167,7 +169,6 @@ class _RecipeViewState extends State { setState(() {}); } - // FIXME: doesnt refresh, should be passed from parent void _removeImage() async { _unsavedRecipe.image = null; await RecipeData.save(); @@ -202,7 +203,7 @@ class _RecipeViewState extends State { padding: const EdgeInsets.only(right: 20.0, left: 20.0), child: TextFormField( controller: _controller1, - style: cRecipeTextStyle, + style: cRecipeTitleStyle, cursorColor: cPrimaryColor, enabled: _isEditingAllowed(), focusNode: _detailsTitleFocus, @@ -226,7 +227,6 @@ class _RecipeViewState extends State { Widget _buildImage() { if (_unsavedRecipe.image != null) { - // TODO: Use https://api.flutter.dev/flutter/widgets/InteractiveViewer-class.html return Padding( padding: const EdgeInsets.only(top: 12.0, left: 20.0, right: 20.0), child: GestureDetector( @@ -257,34 +257,92 @@ class _RecipeViewState extends State { Widget _buildDescriptionInput() { return Padding( padding: const EdgeInsets.all(18.0), - child: TextField( - expands: true, - maxLines: null, - minLines: null, - decoration: null, - controller: _controller2, - cursorColor: cPrimaryColor, - focusNode: _detailsTextFocus, - style: cRecipeDescriptionStyle, - readOnly: !_isEditingAllowed(), - enableInteractiveSelection: true, - toolbarOptions: ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, + child: _isEditingAllowed() ? _buildInputDescriptuion() : _buildMarkdownDescription(), + ); + } + + TextField _buildInputDescriptuion() { + return TextField( + expands: true, + maxLines: null, + minLines: null, + decoration: null, + controller: _controller2, + cursorColor: cPrimaryColor, + focusNode: _detailsTextFocus, + style: cRecipeDescriptionStyle, + enableInteractiveSelection: true, + toolbarOptions: ToolbarOptions( + copy: true, + cut: true, + paste: true, + selectAll: true, + ), + ); + } + + MarkdownBody _buildMarkdownDescription() { + return MarkdownBody( + data: _controller2.text, + selectable: true, + onTapLink: (text, href, title) => null, + imageBuilder: (uri, title, alt) => Container(), + styleSheet: MarkdownStyleSheet( + tableBody: cRecipeDescriptionStyle, + a: cRecipeDescriptionStyle, + p: cRecipeDescriptionStyle, + h1: cRecipeSubtitleStyle, + h2: cRecipeSubtitleStyle, + h3: cRecipeSubtitleStyle, + h4: cRecipeSubtitleStyle, + h5: cRecipeSubtitleStyle, + h6: cRecipeSubtitleStyle, +/* TODO: remove code and quote block + blockquoteDecoration: BoxDecoration(color: Color.fromARGB(255, 255, 13, 13)), + code: cDetailsTextStyle, +*/ + listIndent: 15.0, + listBullet: cRecipeDescriptionStyle, + listBulletPadding: EdgeInsets.only(right: 10.0), + horizontalRuleDecoration: BoxDecoration( + border: Border.all( + color: Colors.grey, + width: 0.5, + ), ), ), ); } - void _shareData() { + void _shareData(BuildContext _) { Share.share("${_unsavedRecipe.title}\n\n${_unsavedRecipe.description}", subject: _unsavedRecipe.title); } - void _addToShoppingList() {} + void _addToShoppingList(BuildContext _) { + final _ingredients = _unsavedRecipe.description!.split("\n")..retainWhere((element) => element.startsWith("- ")); + + for (String _ingredient in _ingredients) { + String _item = _ingredient.substring(2).trim(); + + if (!ShoplistData.shoplist.contains(_item)) { + ShoplistData.shoplist.add(_item); + } + } + + ShoplistData.save(); + + if (widget.showToastCallback != null) { + // TODO: translation + widget.showToastCallback!("Added to Shoplist", "Ok", () { + _addRecipe(passedRecipe: _removedRecipe); + widget.redrawCallback!(); + }); + } + } void _uploadRecipe(BuildContext context) async { + return; + Map _headers = {"Content-Type": "application/json; charset=UTF-8"}; String _body = _unsavedRecipe.toJsonString(); Response? res; @@ -310,13 +368,12 @@ class _RecipeViewState extends State { List _localActions = [ _buildAppBarCheckActions(), PopupMenuButton( - onSelected: (int value) => _actionList.elementAt(value)(), + onSelected: (int value) => _actionList.elementAt(value)(context), itemBuilder: (BuildContext context) => [ - // TODO: Translations - PopupMenuItem(value: 0, child: Text("Teilen")), - PopupMenuItem(value: 1, child: Text("Einkaufen")), - PopupMenuItem(value: 2, child: Text("Hochladen")), - PopupMenuItem(value: 3, child: Text("Löschen")), + PopupMenuItem(value: 0, child: Text(AppLocalizations.of(context)!.menu1)), + PopupMenuItem(value: 1, child: Text(AppLocalizations.of(context)!.menu2)), + PopupMenuItem(value: 2, child: Text(AppLocalizations.of(context)!.menu3)), + PopupMenuItem(value: 3, child: Text(AppLocalizations.of(context)!.menu4)), ], ), ]; @@ -333,12 +390,13 @@ class _RecipeViewState extends State { List _remoteLookupAction = [ IconButton( icon: Icon(Icons.menu_open_rounded), - onPressed: null, // TODO: IMPLEMENT: Show local recipe + onPressed: null, ), ]; if (widget.remote && RecipeData.recipeList.contains(recipe)) return _remoteLookupAction; if (widget.remote) return _remoteDownloadAction; + return _localActions; } diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 7f2ae90..d217bb7 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -15,7 +15,6 @@ class SettingsView extends StatefulWidget { _SettingsViewState createState() => _SettingsViewState(); } -// TODO: Add subtitles class _SettingsViewState extends State { final TextEditingController _controller = TextEditingController(); final FocusNode _focusNode = FocusNode(); @@ -54,7 +53,7 @@ class _SettingsViewState extends State { title: Text(AppLocalizations.of(context)!.setting22, style: cDefaultTextStyle), trailing: IconButton( icon: Icon(Icons.arrow_forward_rounded), - onPressed: null, // TODO: IMPLEMENT: specific server settings + onPressed: null, ), ), _buildTinyTitle(AppLocalizations.of(context)!.settingTitle3), @@ -93,7 +92,7 @@ class _SettingsViewState extends State { ); } - void _updateSettings(String text) { + void _updateSettings(String? text) { _focusNode.unfocus(); if (text != null) { diff --git a/lib/views/shoplist_view.dart b/lib/views/shoplist_view.dart index a9caed8..500b8fc 100644 --- a/lib/views/shoplist_view.dart +++ b/lib/views/shoplist_view.dart @@ -113,7 +113,7 @@ class _ShoplistView extends State { padding: const EdgeInsets.all(15.0), child: TextFormField( controller: _controller, - style: cRecipeTextStyle.copyWith(color: cIconColor), + style: cRecipeTitleStyle.copyWith(color: cIconColor), cursorColor: cIconColor, focusNode: _focusNode, textInputAction: TextInputAction.done, diff --git a/lib/views/vote_view.dart b/lib/views/vote_view.dart index 7de2d10..86ddc0b 100644 --- a/lib/views/vote_view.dart +++ b/lib/views/vote_view.dart @@ -12,14 +12,13 @@ class VoteView extends StatefulWidget { _VoteViewState createState() => _VoteViewState(); } -// TODO: Change from vote to schedule class _VoteViewState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(AppLocalizations.of(context)!.category6)), drawer: CustomDrawer(initalIndex: 3), - body: NoContentError(), // TODO: IMPLEMENT: A voting system + body: NoContentError(), ); } } diff --git a/lib/widgets/recipe_card_widget.dart b/lib/widgets/recipe_card_widget.dart index d96b004..22e50ef 100644 --- a/lib/widgets/recipe_card_widget.dart +++ b/lib/widgets/recipe_card_widget.dart @@ -140,7 +140,7 @@ class __RecipeInfoState extends State<_RecipeInfo> { padding: const EdgeInsets.all(2.0), child: Text( widget.recipe.title!, - style: cRecipeTextStyle, + style: cRecipeTitleStyle, overflow: TextOverflow.ellipsis, ), ), @@ -156,7 +156,7 @@ class __RecipeInfoState extends State<_RecipeInfo> { Padding( padding: const EdgeInsets.only(bottom: 2.0), child: Row( - children: widget.recipe.rating != null ? _buildRating(widget.recipe.rating) : [], + children: _buildRating(widget.recipe.rating), ), ), ], diff --git a/pubspec.lock b/pubspec.lock index 4b370fa..bd333ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -151,6 +151,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.10+2" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -238,6 +245,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.4" + markdown: + dependency: transitive + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 84fb5b2..6b40405 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # 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: 1.3.3 +version: 1.4.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -34,6 +34,7 @@ dependencies: image_picker: ^0.8.5 external_path: ^1.0.1 path_provider: ^2.0.11 + flutter_markdown: ^0.6.10 permission_handler: ^10.0.0 shared_preferences: ^2.0.15 flutter_local_notifications: ^9.7.0 -- cgit v1.2.3