import 'dart:io'; import 'package:flutter/material.dart'; import 'package:kulinar_app/constants.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'; import 'package:kulinar_app/models/data/recipe_data_class.dart'; 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/shoplist_data_class.dart'; import 'package:kulinar_app/models/data/settings_data_class.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:path_provider/path_provider.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, this.recipe, this.redrawCallback, this.showToastCallback}) : super(key: key); final bool remote; final Recipe? recipe; final Function? redrawCallback; final Function? showToastCallback; @override _RecipeViewState createState() => _RecipeViewState(); } class _RecipeViewState extends State { final GlobalKey _formKey = GlobalKey(); final GlobalKey _scaffoldKey = GlobalKey(); final TextEditingController _controller1 = TextEditingController(); final TextEditingController _controller2 = TextEditingController(); final FocusNode _detailsTitleFocus = FocusNode(); final FocusNode _detailsTextFocus = FocusNode(); Recipe _unsavedRecipe = Recipe(); bool _isEditModeEnabled = false; Recipe? _removedRecipe; @override Widget build(BuildContext context) { // TODO: Refactor to easier code if (widget.recipe != null && _unsavedRecipe.isDefault()) _unsavedRecipe = widget.recipe!; if (_unsavedRecipe.isDefault()) { _controller1.clear(); _controller2.clear(); _isEditModeEnabled = true; } else { _controller1.text = _unsavedRecipe.title ?? ""; _controller2.text = _unsavedRecipe.description ?? ""; } return Scaffold( key: _scaffoldKey, appBar: _buildAppBar(context, _unsavedRecipe), body: GestureDetector( onTap: () { _detailsTextFocus.requestFocus(); _controller2.selection = TextSelection.fromPosition(TextPosition(offset: _controller2.text.length)); }, child: Form( key: _formKey, child: ListView( children: [ _buildUtilityRow(), _buildTitleInput(), _buildImage(), _buildDescriptionInput(), ], ), ), ), ); } void _cacheUnsavedRecipe() { _unsavedRecipe.title = _controller1.text; _unsavedRecipe.description = _controller2.text; } void _addRecipe({Recipe? passedRecipe}) { if (passedRecipe != null) { RecipeData.recipeList.add(passedRecipe); RecipeData.save(); return; } if (!_formKey.currentState!.validate()) return; _unsavedRecipe.title = _controller1.text; _unsavedRecipe.description = _controller2.text; if (_isEditModeEnabled) { if (!RecipeData.recipeList.contains(_unsavedRecipe)) RecipeData.recipeList.add(_unsavedRecipe); RecipeData.save(); } _isEditModeEnabled = false; if (widget.recipe == null) { Navigator.pop(context); } else { setState(() {}); } } void _removeRecipe(BuildContext _) { Navigator.pop(context); RecipeData.recipeList.remove(widget.recipe); RecipeData.save(); _removedRecipe = widget.recipe; String _content = AppLocalizations.of(context)!.removed; String _actionLabel = AppLocalizations.of(context)!.undo; if (widget.showToastCallback != null) { widget.showToastCallback!(_content, _actionLabel, () { _addRecipe(passedRecipe: _removedRecipe); widget.redrawCallback!(); }); } setState(() {}); } // TODO: FIXME: This might introduce bugs later (sanetize); maybe use FileHandler? void downloadRecipe(BuildContext context, Recipe recipe) async { RecipeData.recipeList.add(recipe); ToastBar.showToastBar(context, AppLocalizations.of(context)!.downloadSuccess); setState(() {}); } void _pickImage() async { XFile? _pickedFile; if (SettingsData.settings["photoSource"] == "0") { _pickedFile = await ImagePicker().pickImage(source: ImageSource.camera); } else { _pickedFile = await ImagePicker().pickImage(source: ImageSource.gallery); } if (_pickedFile != null) { Directory _directory = await getApplicationDocumentsDirectory(); String _name = DateTime.now().millisecondsSinceEpoch.toString(); File _file = File("${_directory.path}/$_name"); _file.writeAsBytes(await _pickedFile.readAsBytes()); _unsavedRecipe.image = _file.path; RecipeData.save(); } setState(() {}); } void _removeImage() async { _unsavedRecipe.image = null; await RecipeData.save(); setState(() {}); } Widget _buildUtilityRow() { if (widget.remote) { return SizedBox( height: 10.0, width: double.infinity, ); } else { // _buildAppBarPhotoActions(recipe), return UtilityIconRow( remote: widget.remote, isEditModeEnabled: _isEditModeEnabled, unsavedRecipe: _unsavedRecipe, pickImageCallback: _pickImage, removeImageCallback: _removeImage, removeRecipeCallback: _removeRecipe, downloadRecipeCallback: downloadRecipe, cacheUnsavedRecipeCallback: _cacheUnsavedRecipe, ); } } Widget _buildTitleInput() { return Padding( padding: const EdgeInsets.only(right: 20.0, left: 20.0), child: TextFormField( controller: _controller1, style: cRecipeTitleStyle, cursorColor: cPrimaryColor, enabled: _isEditModeEnabled, focusNode: _detailsTitleFocus, textInputAction: TextInputAction.next, decoration: InputDecoration( hintText: AppLocalizations.of(context)!.inputHint, hintStyle: cInputHintStyle, ), onFieldSubmitted: (_) { _detailsTitleFocus.unfocus(); FocusScope.of(context).requestFocus(_detailsTextFocus); }, validator: (input) { if (input == "" || input!.trim().isEmpty) return AppLocalizations.of(context)!.inputError; return null; }, ), ); } Widget _buildImage() { if (_unsavedRecipe.image != null) { return Padding( padding: const EdgeInsets.only(top: 12.0, left: 20.0, right: 20.0), child: GestureDetector( child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: Hero( tag: "image", child: Image( image: FileImage( File(_unsavedRecipe.image!), ), ), ), ), onTap: () { Navigator.push( context, FadeRoute(child: ImageView(image: _unsavedRecipe.image!)), ); }, ), ); } return Container(); } Widget _buildDescriptionInput() { return Padding( padding: const EdgeInsets.all(18.0), child: _isEditModeEnabled ? _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: CustomMarkdownStyle(), ); } void _shareData(BuildContext _) { Share.share("${_unsavedRecipe.title}\n\n${_unsavedRecipe.description}", subject: _unsavedRecipe.title); } void _addToShoppingList(BuildContext _) { final _ingredients = _unsavedRecipe.description!.split("\n")..retainWhere((element) => element.startsWith("- ")); if (_ingredients.isEmpty) { // TODO: translation widget.showToastCallback!("No ingredients found", ""); return; } 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", ""); } } void _uploadRecipe(BuildContext context) async { return; Map _headers = {"Content-Type": "application/json; charset=UTF-8"}; String _body = _unsavedRecipe.toJsonString(); Response? res; try { res = await post(Uri.https(SettingsData.settings["serverURL"]!, "/"), headers: _headers, body: _body); } catch (e) { print(e); } finally { if (res != null && res.statusCode == 200) { ToastBar.showToastBar(context, AppLocalizations.of(context)!.uploadSuccess); } else { ToastBar.showToastBar(context, AppLocalizations.of(context)!.uploadError); } setState(() {}); } } List _buildAppBarActions(BuildContext context, Recipe recipe) { final _actionList = [_shareData, _addToShoppingList, _uploadRecipe, _removeRecipe]; List _localActions = [ _buildAppBarCheckActions(), PopupMenuButton( onSelected: (int value) => _actionList.elementAt(value)(context), itemBuilder: (BuildContext context) => [ 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)), ], ), ]; List _remoteDownloadAction = [ IconButton( icon: Icon(Icons.save_alt_rounded), onPressed: () { downloadRecipe(context, recipe); }, ), ]; List _remoteLookupAction = [ IconButton( icon: Icon(Icons.menu_open_rounded), onPressed: null, ), ]; if (widget.remote && RecipeData.recipeList.contains(recipe)) return _remoteLookupAction; if (widget.remote) return _remoteDownloadAction; return _localActions; } PreferredSizeWidget _buildAppBar(BuildContext context, Recipe recipe) { String _title = AppLocalizations.of(context)!.mode1; if (_isEditModeEnabled) _title = AppLocalizations.of(context)!.mode2; return AppBar( title: Text(_title), leading: IconButton( icon: Icon(Icons.close), onPressed: () { Navigator.pop(context); }, ), actions: _buildAppBarActions(context, recipe), ); } Widget _buildAppBarCheckActions() { if (_isEditModeEnabled) { return IconButton( icon: Icon(Icons.check), onPressed: _addRecipe, ); } else { return IconButton( icon: Icon(Icons.edit), onPressed: () { _isEditModeEnabled = true; setState(() {}); }, ); } } }