diff options
Diffstat (limited to 'lib/widgets')
-rw-r--r-- | lib/widgets/custom_drawer_widget.dart | 129 | ||||
-rw-r--r-- | lib/widgets/error_widgets.dart | 97 | ||||
-rw-r--r-- | lib/widgets/page_route_transitions.dart | 65 | ||||
-rw-r--r-- | lib/widgets/recipe_card_widget.dart | 178 | ||||
-rw-r--r-- | lib/widgets/recipe_search_delegate.dart | 112 | ||||
-rw-r--r-- | lib/widgets/toastbar_widget.dart | 27 | ||||
-rw-r--r-- | lib/widgets/utility_icon_row_widget.dart | 187 |
7 files changed, 795 insertions, 0 deletions
diff --git a/lib/widgets/custom_drawer_widget.dart b/lib/widgets/custom_drawer_widget.dart new file mode 100644 index 0000000..3fd8c2e --- /dev/null +++ b/lib/widgets/custom_drawer_widget.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; + +import 'package:kulinar_app/constants.dart'; +import 'package:kulinar_app/views/info_view.dart'; +import 'package:kulinar_app/views/main_view.dart'; +import 'package:kulinar_app/views/vote_view.dart'; +import 'package:kulinar_app/views/week_view.dart'; +import 'package:kulinar_app/views/shoplist_view.dart'; +import 'package:kulinar_app/views/settings_view.dart'; +import 'package:kulinar_app/views/favorites_view.dart'; +import 'package:kulinar_app/models/data/settings_data_class.dart'; +import 'package:kulinar_app/widgets/page_route_transitions.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class CustomDrawer extends StatefulWidget { + CustomDrawer({Key? key, required this.initalIndex}) : super(key: key); + + final int initalIndex; + + @override + _CustomDrawerState createState() => _CustomDrawerState(); +} + +class _CustomDrawerState extends State<CustomDrawer> { + int? _index; + + @override + void initState() { + super.initState(); + _index = widget.initalIndex; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Drawer( + child: Column( + children: [ + Expanded( + child: Column( + children: [ + _buildDrawerHeader(), + _buildDrawerItem(0, Icons.receipt_rounded, AppLocalizations.of(context)!.category1, () { + _navigateTo(MainView(), 0); + }), + _buildDrawerItem(1, Icons.favorite_rounded, AppLocalizations.of(context)!.category4, () { + _navigateTo(FavoritesView(), 1); + }), + _buildDrawerItem(2, Icons.calendar_today_rounded, AppLocalizations.of(context)!.category5, () { + _navigateTo(WeekView(), 2); + }), + _buildDrawerItem(3, Icons.how_to_vote_rounded, AppLocalizations.of(context)!.category6, () { + _navigateTo(VoteView(), 3); + }), + _buildDrawerItem(4, Icons.shopping_cart_rounded, AppLocalizations.of(context)!.category7, () { + _navigateTo(ShoplistView(), 4); + }), + ], + ), + ), + _buildDrawerItem(5, Icons.settings_rounded, AppLocalizations.of(context)!.category8, () { + _navigateTo(SettingsView(), 5); + }), + _buildDrawerItem(6, Icons.info_rounded, AppLocalizations.of(context)!.category9, () { + _navigateTo(InfoView(), 6); + }), + ], + ), + ); + } + + Widget _buildDrawerHeader() { + return SizedBox( + width: double.infinity, + child: DrawerHeader( + margin: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Icon( + Icons.restaurant_menu_rounded, + color: cIconColor, + size: 100.0, + ), + ), + ], + ), + decoration: BoxDecoration(color: cPrimaryColor), + ), + ); + } + + Widget _buildDrawerItem(int index, IconData icon, String title, Function callback) { + Color color = cPassiveDrawerColor; + + if (index == _index) { + color = cPrimaryColor; + } + + if ((index == 2 || index == 3) && SettingsData.settings["serverURL"] == "") return Container(); + + return ListTile( + leading: Icon(icon, color: color), + title: Text(title, style: cDrawerTextStyle.copyWith(color: color)), + onTap: () { + callback(); + }, + ); + } + + void _navigateTo(Widget route, int index) async { + _index = index; + + Navigator.pop(context); + + await Navigator.pushReplacement( + context, + FadeRoute(child: route), + ); + } +} diff --git a/lib/widgets/error_widgets.dart b/lib/widgets/error_widgets.dart new file mode 100644 index 0000000..52ac0bb --- /dev/null +++ b/lib/widgets/error_widgets.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import 'package:kulinar_app/constants.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class NoContentError extends StatelessWidget { + const NoContentError({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.sentiment_dissatisfied_rounded, + size: 100, + ), + Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Text(AppLocalizations.of(context)!.noContentError, style: cZeroContentStyle), + ), + ], + ), + ); + } +} + +class NetworkContentError extends StatelessWidget { + const NetworkContentError({Key? key, required this.refreshCallback}) : super(key: key); + + final Function refreshCallback; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(50.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.signal_wifi_off_rounded, + size: 100, + ), + Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Text(AppLocalizations.of(context)!.noNetworkError, textAlign: TextAlign.center, style: cZeroContentStyle), + ), + Padding( + padding: const EdgeInsets.only(top: 20.0), + child: ElevatedButton( + child: Padding( + padding: const EdgeInsets.only(left: 50.0, right: 50.0), + child: Text(AppLocalizations.of(context)!.retry, style: cSubTitleStyle), + ), + onPressed: () { + refreshCallback(); + }, + ), + ), + ], + ), + ), + ); + } +} + +class UnknownError extends StatelessWidget { + const UnknownError({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(50.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.warning_amber_rounded, + size: 100, + ), + Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Text(AppLocalizations.of(context)!.unknownError, textAlign: TextAlign.center, style: cZeroContentStyle), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/page_route_transitions.dart b/lib/widgets/page_route_transitions.dart new file mode 100644 index 0000000..8491555 --- /dev/null +++ b/lib/widgets/page_route_transitions.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class SlideFromLeftRoute extends PageRouteBuilder { + final Widget child; + + SlideFromLeftRoute({required this.child}) + : super( + transitionDuration: Duration(milliseconds: 300), + pageBuilder: (BuildContext _, Animation<double> __, Animation<double> ___) => child, + transitionsBuilder: (BuildContext _, Animation<double> animation, Animation<double> __, Widget child) => SlideTransition( + position: Tween<Offset>( + begin: Offset(-1, 0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: animation, curve: Interval(0.00, 1.00, curve: Curves.ease))), + child: child, + ), + ); +} + +class SlideFromRightRoute extends PageRouteBuilder { + final Widget child; + + SlideFromRightRoute({required this.child}) + : super( + transitionDuration: Duration(milliseconds: 300), + pageBuilder: (BuildContext _, Animation<double> __, Animation<double> ___) => child, + transitionsBuilder: (BuildContext _, Animation<double> animation, Animation<double> __, Widget child) => SlideTransition( + position: Tween<Offset>( + begin: Offset(1, 0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: animation, curve: Interval(0.00, 1.00, curve: Curves.ease))), + child: child, + ), + ); +} + +class SlideFromBottomRoute extends PageRouteBuilder { + final Widget child; + + SlideFromBottomRoute({required this.child}) + : super( + transitionDuration: Duration(milliseconds: 300), + pageBuilder: (BuildContext _, Animation<double> __, Animation<double> ___) => child, + transitionsBuilder: (BuildContext _, Animation<double> animation, Animation<double> __, Widget child) => SlideTransition( + position: Tween<Offset>( + begin: Offset(0, 1), + end: Offset.zero, + ).animate(CurvedAnimation(parent: animation, curve: Interval(0.00, 1.00, curve: Curves.ease))), + child: child, + ), + ); +} + +class FadeRoute extends PageRouteBuilder { + final Widget child; + + FadeRoute({required this.child}) + : super( + transitionDuration: Duration(milliseconds: 300), + pageBuilder: (BuildContext _, Animation<double> animation, Animation<double> __) => FadeTransition( + opacity: animation, + child: child, + ), + ); +} diff --git a/lib/widgets/recipe_card_widget.dart b/lib/widgets/recipe_card_widget.dart new file mode 100644 index 0000000..d96b004 --- /dev/null +++ b/lib/widgets/recipe_card_widget.dart @@ -0,0 +1,178 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'package:kulinar_app/constants.dart'; +import 'package:kulinar_app/models/data/recipe_data_class.dart'; +import 'package:kulinar_app/models/data/settings_data_class.dart'; +import 'package:kulinar_app/views/recipe_view.dart'; +import 'package:kulinar_app/models/recipe_class.dart'; +import 'package:kulinar_app/widgets/page_route_transitions.dart'; + +class RecipeCard extends StatefulWidget { + const RecipeCard({Key? key, this.remote = false, this.inSearch = false, required this.recipe, this.redrawCallback, this.showToastCallback}) : super(key: key); + + final bool remote; + final bool inSearch; + final Recipe recipe; + final Function? redrawCallback; + final Function? showToastCallback; + + @override + _RecipeCardState createState() => _RecipeCardState(); +} + +class _RecipeCardState extends State<RecipeCard> { + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _recipeCardPressed, + child: Padding( + padding: const EdgeInsets.only(top: 5.0, right: 5.0, left: 5.0), + child: Card( + elevation: 30.0, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + _RecipeInfo(recipe: widget.recipe), + _buildImage(), + ], + ), + ), + ), + ), + ); + } + + Future<void> _recipeCardPressed() async { + await Navigator.push( + context, + SlideFromBottomRoute( + child: RecipeView( + remote: widget.remote, + readonly: true, + recipe: widget.recipe, + redrawCallback: widget.redrawCallback, + showToastCallback: widget.showToastCallback, + ), + ), + ); + + try { + if (!mounted || widget.redrawCallback == null) return; + + widget.redrawCallback!(); + } catch (e) { + print(e); + } + } + + Widget _buildImage() { + if (widget.recipe.image == null) return Container(); + if (SettingsData.settings["showPhotos"] == "3") return Container(); + if (SettingsData.settings["showPhotos"] == "1" && !widget.inSearch) return Container(); + if (SettingsData.settings["showPhotos"] == "2" && widget.inSearch) return Container(); + + return Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Container( + height: 200.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.0), + image: DecorationImage( + image: FileImage( + File(widget.recipe.image!), + ), + fit: BoxFit.cover, + ), + ), + ), + ); + } +} + +class _RecipeInfo extends StatefulWidget { + _RecipeInfo({Key? key, required this.recipe}) : super(key: key); + + final Recipe recipe; + + @override + __RecipeInfoState createState() => __RecipeInfoState(); +} + +class __RecipeInfoState extends State<_RecipeInfo> { + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildRecipeInfo(), + _buildFavortiteIcon(), + ], + ); + } + + List<Widget> _buildRating(int rating) { + List<Widget> _result = []; + + if (rating == 0) return _result; + + for (int i = 0; i < 5; i++) { + _result.add(rating >= 1 ? Icon(Icons.star_rounded) : Icon(Icons.star_border_rounded)); + rating--; + } + + return _result; + } + + Widget _buildRecipeInfo() { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(2.0), + child: Text( + widget.recipe.title!, + style: cRecipeTextStyle, + overflow: TextOverflow.ellipsis, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 2.0, left: 2.0), + child: Text( + widget.recipe.description!, + style: cRecipeDescriptionStyle, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 2.0), + child: Row( + children: widget.recipe.rating != null ? _buildRating(widget.recipe.rating) : [], + ), + ), + ], + ), + ); + } + + Widget _buildFavortiteIcon() { + return IconButton( + icon: widget.recipe.favorite ? Icon(Icons.favorite_rounded, color: Colors.red) : Icon(Icons.favorite_border_rounded), + onPressed: () { + widget.recipe.toggleFavorite(); + RecipeData.save(); + + setState(() {}); + }, + ); + } +} diff --git a/lib/widgets/recipe_search_delegate.dart b/lib/widgets/recipe_search_delegate.dart new file mode 100644 index 0000000..1cb02e7 --- /dev/null +++ b/lib/widgets/recipe_search_delegate.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; + +import 'package:kulinar_app/constants.dart'; +import 'package:kulinar_app/models/recipe_class.dart'; +import 'package:kulinar_app/widgets/recipe_card_widget.dart'; +import 'package:kulinar_app/models/data/recipe_data_class.dart'; + +class RecipeSearch extends SearchDelegate { + @override + List<Widget> buildActions(BuildContext context) { + return [ + IconButton( + icon: Icon(Icons.clear), + onPressed: () { + query = ""; + }, + ), + ]; + } + + @override + Widget buildLeading(BuildContext context) { + return IconButton( + icon: Icon(Icons.arrow_back, color: cIconColor), + onPressed: () { + close(context, null); + }, + ); + } + + @override + Widget buildResults(BuildContext context) { + List<Recipe> _filteredRecipeList = RecipeData.recipeList.where((element) => element.title!.toLowerCase().contains(query.toLowerCase())).toList(); + + return ListView.builder( + itemCount: _filteredRecipeList.length, + itemBuilder: (context, index) => RecipeCard(inSearch: true, recipe: _filteredRecipeList[index]), + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + List<Recipe> _filteredRecipeList = RecipeData.recipeList.where((element) => element.title!.toLowerCase().contains(query.toLowerCase())).toList(); + + return ListView.builder( + itemCount: _filteredRecipeList.length, + itemBuilder: (context, index) => RecipeCard(inSearch: true, recipe: _filteredRecipeList[index]), + ); + } + + @override + ThemeData appBarTheme(BuildContext context) { + return Theme.of(context).copyWith( + textTheme: TextTheme( + headline6: cSearchTextStyle, + ), + ); + } +} + +class FavoriteRecipeSearch extends SearchDelegate { + @override + List<Widget> buildActions(BuildContext context) { + return [ + IconButton( + icon: Icon(Icons.clear), + onPressed: () { + query = ""; + }, + ), + ]; + } + + @override + Widget buildLeading(BuildContext context) { + return IconButton( + icon: Icon(Icons.arrow_back, color: cIconColor), + onPressed: () { + close(context, null); + }, + ); + } + + @override + Widget buildResults(BuildContext context) { + List<Recipe> _filteredRecipeList = RecipeData.recipeList.where((element) => element.title!.toLowerCase().contains(query.toLowerCase()) && element.favorite).toList(); + + return ListView.builder( + itemCount: _filteredRecipeList.length, + itemBuilder: (context, index) => RecipeCard(inSearch: true, recipe: _filteredRecipeList[index]), + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + List<Recipe> _filteredRecipeList = RecipeData.recipeList.where((element) => element.title!.toLowerCase().contains(query.toLowerCase()) && element.favorite).toList(); + + return ListView.builder( + itemCount: _filteredRecipeList.length, + itemBuilder: (context, index) => RecipeCard(inSearch: true, recipe: _filteredRecipeList[index]), + ); + } + + @override + ThemeData appBarTheme(BuildContext context) { + return Theme.of(context).copyWith( + textTheme: TextTheme( + headline6: cSearchTextStyle, + ), + ); + } +} diff --git a/lib/widgets/toastbar_widget.dart b/lib/widgets/toastbar_widget.dart new file mode 100644 index 0000000..760ce2e --- /dev/null +++ b/lib/widgets/toastbar_widget.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class ToastBar { + static Future<void> showToastBar(BuildContext context, String content, {bool error = false, String? actionLabel, Function? actionCallback}) async { + SnackBar snackBar = SnackBar( + action: actionLabel != "" + ? SnackBarAction( + label: actionLabel!, + onPressed: () { + if (actionCallback != null) actionCallback(); + }, + textColor: Theme.of(context).colorScheme.secondary) + : null, + backgroundColor: error == true ? Colors.red : Colors.grey[900], + behavior: SnackBarBehavior.floating, + duration: Duration(seconds: 5), + elevation: 5.0, + content: Text( + content, + style: TextStyle(color: Colors.white), + overflow: TextOverflow.ellipsis, + ), + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } +} diff --git a/lib/widgets/utility_icon_row_widget.dart b/lib/widgets/utility_icon_row_widget.dart new file mode 100644 index 0000000..3d45d15 --- /dev/null +++ b/lib/widgets/utility_icon_row_widget.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; + +import 'package:kulinar_app/constants.dart'; +import 'package:kulinar_app/models/data/settings_data_class.dart'; +import 'package:kulinar_app/models/recipe_class.dart'; +import 'package:kulinar_app/models/data/recipe_data_class.dart'; + +class UtilityIconRow extends StatefulWidget { + const UtilityIconRow({ + Key? key, + this.remote = false, + required this.readonly, + required this.unsavedRecipe, + required this.pickImageCallback, + required this.removeImageCallback, + required this.removeRecipeCallback, + required this.downloadRecipeCallback, + required this.isEditingAllowedCallback, + required this.cacheUnsavedRecipeCallback, + }) : super(key: key); + + final bool remote; + final bool readonly; + final Recipe unsavedRecipe; + final Function pickImageCallback; + final Function removeImageCallback; + final Function removeRecipeCallback; + final Function downloadRecipeCallback; + final Function isEditingAllowedCallback; + final Function cacheUnsavedRecipeCallback; + + @override + _UtilityIconRowState createState() => _UtilityIconRowState(); +} + +class _UtilityIconRowState extends State<UtilityIconRow> { + bool _isInputSourceCamera = SettingsData.settings["photoSource"] == "0" ? true : false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(0), // EdgeInsets.only(left: 4.0), + child: _buildButtonAddImage(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _buildIconRating(context), + _buildIconFavorite(), + // _buildIconCalculate(), + // _buildIconShare(), + // _buildIconImportExport(context), + ], + ), + ], + ), + ); + } + + IconButton _buildButtonAddImage() { + Icon _pickImageIcon; + Icon _removeImageIcon; + bool _recipeHasImage = widget.unsavedRecipe.image == null; + + if (_isInputSourceCamera) { + _pickImageIcon = Icon(Icons.add_a_photo_rounded); + _removeImageIcon = Icon(Icons.no_photography_rounded); + } else { + _pickImageIcon = Icon(Icons.add_photo_alternate_rounded); + _removeImageIcon = Icon(Icons.image_not_supported_rounded); + } + + void _onPress() { + if (_recipeHasImage) { + widget.pickImageCallback(); + } else { + widget.removeImageCallback(); + } + } + + return IconButton( + icon: _recipeHasImage ? _pickImageIcon : _removeImageIcon, + onPressed: _onPress, + ); + } + + Widget _buildIconRating(BuildContext context) { + void _onPress() { + widget.unsavedRecipe.updateRating(); + widget.cacheUnsavedRecipeCallback(); + RecipeData.save(); + + setState(() {}); + } + + return Row( + children: [ + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text(widget.unsavedRecipe.rating.toString(), style: cDefaultTextStyle), + ), + IconButton( + iconSize: 28.0, + icon: Icon(Icons.star_border_rounded), + onPressed: !widget.remote ? _onPress : null, + ), + ], + ); + } + + Widget _buildIconFavorite() { + Icon icon = Icon(Icons.favorite_border_rounded); + Color color = Colors.black; + + if (widget.unsavedRecipe.favorite) { + icon = Icon(Icons.favorite_rounded); + color = Colors.red; + } + + void _onPress() { + widget.unsavedRecipe.toggleFavorite(); + widget.cacheUnsavedRecipeCallback(); + RecipeData.save(); + + setState(() {}); + } + + return IconButton( + icon: icon, + color: color, + onPressed: !widget.remote ? _onPress : null, + ); + } + + /* todo: IMPLEMENT RICH TEXT https://stackoverflow.com/questions/41557139/how-do-i-bold-or-format-a-piece-of-text-within-a-paragraph + // todo: Implement recalculations (portions, measurement system) + Widget _buildIconCalculate() { + return IconButton( + icon: Icon(Icons.calculate_rounded), + onPressed: null, + ); + } + */ + +/* Widget _buildIconShare() { + return IconButton( + icon: Icon(Icons.share), + onPressed: widget.readonly && !widget.remote ? _shareData : null, + ); + } */ + + /* + IF REMOTE: + DOWNLOAD + + IF REMOTE AND LOCAL: + GO TO LOCAL + + IF LOCAL: + UPLOAD + Widget _buildIconImportExport(BuildContext context) { + if (widget.unsavedRecipe.isListed(remote: true)) { + if (widget.unsavedRecipe.isListed()) { + return IconButton( + icon: Icon(Icons.get_app), + onPressed: !widget.isEditingAllowedCallback() ? () => widget.downloadRecipeCallback(context, widget.unsavedRecipe) : null, + ); + } else { + return IconButton( + icon: Icon(Icons.arrow_back), + onPressed: !widget.isEditingAllowedCallback() ? () => widget.downloadRecipeCallback(context, widget.unsavedRecipe) : null, + ); + } + } else { + return IconButton( + icon: Icon(Icons.publish), + onPressed: !widget.isEditingAllowedCallback() ? () => _uploadRecipe(context) : null, + ); + } + } + */ +} |