aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/constants.dart90
-rw-r--r--lib/l10n/app_de.arb56
-rw-r--r--lib/l10n/app_en.arb218
-rw-r--r--lib/l10n/app_ru.arb56
-rw-r--r--lib/main.dart88
-rw-r--r--lib/models/data/recipe_data_class.dart52
-rw-r--r--lib/models/data/settings_data_class.dart33
-rw-r--r--lib/models/data/shoplist_data_class.dart31
-rw-r--r--lib/models/recipe_class.dart63
-rw-r--r--lib/util/file_handler.dart109
-rw-r--r--lib/util/notifications.dart32
-rw-r--r--lib/util/storage_handler.dart20
-rw-r--r--lib/views/favorites_view.dart82
-rw-r--r--lib/views/file_info.dart103
-rw-r--r--lib/views/image_view.dart39
-rw-r--r--lib/views/info_view.dart89
-rw-r--r--lib/views/main_view.dart141
-rw-r--r--lib/views/recipe_view.dart379
-rw-r--r--lib/views/settings_view.dart184
-rw-r--r--lib/views/shoplist_view.dart167
-rw-r--r--lib/views/vote_view.dart25
-rw-r--r--lib/views/week_view.dart81
-rw-r--r--lib/widgets/custom_drawer_widget.dart129
-rw-r--r--lib/widgets/error_widgets.dart97
-rw-r--r--lib/widgets/page_route_transitions.dart65
-rw-r--r--lib/widgets/recipe_card_widget.dart178
-rw-r--r--lib/widgets/recipe_search_delegate.dart112
-rw-r--r--lib/widgets/toastbar_widget.dart27
-rw-r--r--lib/widgets/utility_icon_row_widget.dart187
29 files changed, 2933 insertions, 0 deletions
diff --git a/lib/constants.dart b/lib/constants.dart
new file mode 100644
index 0000000..66d0257
--- /dev/null
+++ b/lib/constants.dart
@@ -0,0 +1,90 @@
+import 'package:flutter/material.dart';
+
+const cVersion = "1.3.2";
+
+// Colors
+const cIconColor = Colors.white;
+const cPrimaryColor = Colors.lightGreen;
+const cPassiveDrawerColor = Colors.black54;
+const cPrimarySwatchColor = Colors.lightGreen;
+
+// Sans-serif styles
+const cTitleStyle = TextStyle(
+ fontFamily: "Montserrat",
+ color: Colors.white,
+ fontSize: 24.0,
+);
+
+const cSearchTextStyle = TextStyle(
+ fontFamily: "Montserrat",
+ color: Colors.white,
+ fontSize: 18.0,
+);
+
+const cSubTitleStyle = TextStyle(
+ fontFamily: "Montserrat",
+ color: Colors.white,
+ fontSize: 14.0,
+);
+
+const cInputHintStyle = TextStyle(
+ fontFamily: "Montserrat",
+ color: Colors.grey,
+ fontSize: 14.0,
+);
+
+const cRecipeTextStyle = TextStyle(
+ color: Colors.black,
+ fontFamily: "Montserrat",
+ fontSize: 18.0,
+);
+
+const cDefaultTextStyle = TextStyle(
+ fontFamily: "Montserrat",
+ fontSize: 16.0,
+);
+
+const cOptionTextStyle = TextStyle(
+ fontFamily: "Montserrat",
+ fontSize: 14.0,
+);
+
+const cZeroContentStyle = TextStyle(
+ fontFamily: "Montserrat",
+ fontSize: 20.0,
+);
+
+const cTableKeyStyle = TextStyle(
+ fontFamily: "Montserrat",
+ color: cPrimaryColor,
+ fontSize: 16.0,
+);
+
+const cTableValueStyle = TextStyle(
+ fontFamily: "Montserrat",
+ fontSize: 16.0,
+);
+
+// Serif styles
+const cDrawerTextStyle = TextStyle(
+ color: cPassiveDrawerColor,
+ fontFamily: "RobotoSlab",
+ fontSize: 16.0,
+);
+
+const cTinyTitleStyle = TextStyle(
+ fontFamily: "RobotoSlab",
+ color: cPrimaryColor,
+ fontSize: 15.0,
+);
+
+const cRecipeDescriptionStyle = TextStyle(
+ fontFamily: "RobotoSlab",
+ color: Colors.black54,
+ fontSize: 14.0,
+);
+
+const cDetailsTextStyle = TextStyle(
+ fontFamily: "RobotoSlab",
+ fontSize: 14.0,
+);
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
new file mode 100644
index 0000000..f566c6e
--- /dev/null
+++ b/lib/l10n/app_de.arb
@@ -0,0 +1,56 @@
+{
+ "category1": "Rezepte",
+ "category2": "Bewertet",
+ "category3": "Neu",
+ "category4": "Favoriten",
+ "category5": "Diese Woche",
+ "category6": "Abstimmung",
+ "category7": "Einkaufsliste",
+ "category8": "Einstellungen",
+ "category9": "Info",
+ "mode1": "Details",
+ "mode2": "Bearbeiten",
+ "downloadSuccess": "Download erfolgreich.",
+ "uploadError": "Upload fehlgeschlagen.",
+ "uploadSuccess": "Upload erfolgreich.",
+ "exportError": "Export fehlgeschlagen.",
+ "exportSuccess": "Export erfolgreich.",
+ "importError": "Import fehlgeschlagen.",
+ "importSuccess": "Import erfolgreich.",
+ "tapHint": "Zum anzeigen tippen.",
+ "inputError": "Darf nicht leer sein.",
+ "inputHint": "Titel",
+ "info1": "Version",
+ "info2": "Entwickler",
+ "info3": "Webseite",
+ "info4": "Quellcode",
+ "info5": "Lizenzhinweis",
+ "settingTitle1": "Anzeige",
+ "settingTitle2": "Bedienung",
+ "settingTitle3": "Daten",
+ "setting11": "Fotos anzeigen",
+ "setting21": "Bildquelle",
+ "setting22": "Servereinstellungen",
+ "setting31": "Als Datei exportieren",
+ "setting32": "Aus Datei importieren",
+ "option111": "Kamera",
+ "option112": "Galerie",
+ "option211": "Immer",
+ "option212": "In Suche",
+ "option213": "In Listen",
+ "option214": "Nur im Rezept",
+ "option311": "Exportieren",
+ "option312": "Importieren",
+ "infoField1": "Typ",
+ "infoField2": "Version",
+ "infoField3": "Größe",
+ "infoField4": "Einträge",
+ "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.",
+ "unknownError": "Etwas ist schief gelaufen.",
+ "unknown": "Unbekannt",
+ "removed": "Eintrag entfernt",
+ "retry": "Erneut versuchen",
+ "undo": "Zurück"
+} \ No newline at end of file
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
new file mode 100644
index 0000000..930595d
--- /dev/null
+++ b/lib/l10n/app_en.arb
@@ -0,0 +1,218 @@
+{
+ "category1": "Recipes",
+ "@category1": {
+ "description": "A category name"
+ },
+ "category2": "Rated",
+ "@category2": {
+ "description": "A category name"
+ },
+ "category3": "New",
+ "@category3": {
+ "description": "A category name"
+ },
+ "category4": "Favorites",
+ "@category4": {
+ "description": "A category name"
+ },
+ "category5": "This Week",
+ "@category5": {
+ "description": "A category name"
+ },
+ "category6": "Voting",
+ "@category6": {
+ "description": "A category name"
+ },
+ "category7": "Shopping List",
+ "@category7": {
+ "description": "A category name"
+ },
+ "category8": "Settings",
+ "@category8": {
+ "description": "A category name"
+ },
+ "category9": "Info",
+ "@category9": {
+ "description": "A category name"
+ },
+ "mode1": "Details",
+ "@mode1": {
+ "description": "A name for a mode to view recipes"
+ },
+ "mode2": "Edit",
+ "@mode2": {
+ "description": "A name for a mode to edit recipes"
+ },
+ "downloadSuccess": "Download successful.",
+ "@downloadSuccess": {
+ "description": "The text when a download succeedes"
+ },
+ "uploadError": "Upload failed.",
+ "@uploadError": {
+ "description": "The text when an upload fails"
+ },
+ "uploadSuccess": "Upload successful.",
+ "@uploadSuccess": {
+ "description": "The text when an upload succeedes"
+ },
+ "exportError": "Export failed.",
+ "@exportError": {
+ "description": "The text when an export fails"
+ },
+ "exportSuccess": "Export successful.",
+ "@exportSuccess": {
+ "description": "The text when an export succeedes"
+ },
+ "importError": "Import failed.",
+ "@importError": {
+ "description": "The text when an import fails"
+ },
+ "importSuccess": "Import successful.",
+ "@importSuccess": {
+ "description": "The text when an import succeedes"
+ },
+ "tapHint": "Tap to show.",
+ "@tapHint": {
+ "description": "The notification hint to tap on it"
+ },
+ "inputError": "Must not be empty.",
+ "@inputError": {
+ "description": "The text when a specific input is left empty"
+ },
+ "inputHint": "Title",
+ "@inputHint": {
+ "description": "A hint text for an input field"
+ },
+ "info1": "Version",
+ "@info1": {
+ "description": "A field name in the info panel"
+ },
+ "info2": "Developer",
+ "@info2": {
+ "description": "A field name in the info panel"
+ },
+ "info3": "Website",
+ "@info3": {
+ "description": "A field name in the info panel"
+ },
+ "info4": "Source Code",
+ "@info4": {
+ "description": "A field name in the info panel"
+ },
+ "info5": "Legal notice",
+ "@info5": {
+ "description": "A field name in the info panel"
+ },
+ "settingTitle1": "Display",
+ "@settingTitle1": {
+ "description": "A title for a section in the settings"
+ },
+ "settingTitle2": "Operation",
+ "@settingTitle2": {
+ "description": "A title for a section in the settings"
+ },
+ "settingTitle3": "Data",
+ "@settingTitle3": {
+ "description": "A title for a section in the settings"
+ },
+ "setting11": "Show photos",
+ "@setting11": {
+ "description": "A setting"
+ },
+ "setting21": "Image source",
+ "@setting21": {
+ "description": "A setting"
+ },
+ "setting22": "Server settings",
+ "@setting22": {
+ "description": "A setting"
+ },
+ "setting31": "Export to File",
+ "@setting31": {
+ "description": "A setting"
+ },
+ "setting32": "Import form file",
+ "@setting32": {
+ "description": "A setting"
+ },
+ "option111": "Camera",
+ "@option111": {
+ "description": "A setting option"
+ },
+ "option112": "Gallery",
+ "@option112": {
+ "description": "A setting option"
+ },
+ "option211": "Always",
+ "@option211": {
+ "description": "A setting option"
+ },
+ "option212": "In search",
+ "@option212": {
+ "description": "A setting option"
+ },
+ "option213": "In lists",
+ "@option213": {
+ "description": "A setting option"
+ },
+ "option214": "Only in recipe",
+ "@option214": {
+ "description": "A setting option"
+ },
+ "option311": "Export",
+ "@option311": {
+ "description": "A setting option"
+ },
+ "option312": "Import",
+ "@option312": {
+ "description": "A setting option"
+ },
+ "infoField1": "Type",
+ "@infoField1": {
+ "description": "A file info text field"
+ },
+ "infoField2": "Version",
+ "@infoField2": {
+ "description": "A file info text field"
+ },
+ "infoField3": "Size",
+ "@infoField3": {
+ "description": "A file info text field"
+ },
+ "infoField4": "Entries",
+ "@infoField4": {
+ "description": "A file info text field"
+ },
+ "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"
+ },
+ "noContentError": "Nothing to see yet.",
+ "@noContentError": {
+ "description": "The text when no content can be displayed"
+ },
+ "noNetworkError": "Error while fetching recipes.",
+ "@noNetworkError": {
+ "description": "The text when a server request fails"
+ },
+ "unknownError": "Something went wrong.",
+ "@unknownError": {
+ "description": "The text when an unknown error occurs"
+ },
+ "unknown": "Unknown",
+ "@unknown": {
+ "description": "The text when information is unknown"
+ },
+ "removed": "Entry removed",
+ "@removed": {
+ "description": "The text when any entry is removed"
+ },
+ "retry": "Retry",
+ "@retry": {
+ "description": "The prompt to retry"
+ },
+ "undo": "Back",
+ "@undo": {
+ "description": "The prompt to undo"
+ }
+} \ No newline at end of file
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
new file mode 100644
index 0000000..6b74dd7
--- /dev/null
+++ b/lib/l10n/app_ru.arb
@@ -0,0 +1,56 @@
+{
+ "category1": "Рецепты",
+ "category2": "Оценены",
+ "category3": "Новые",
+ "category4": "Фавориты",
+ "category5": "Эту неделю",
+ "category6": "Голoсование",
+ "category7": "Список покупок",
+ "category8": "Настроики",
+ "category9": "Инфо",
+ "mode1": "Детали",
+ "mode2": "Писать",
+ "downloadSuccess": "Загруска удолась.",
+ "uploadError": "Загруска не удолась.",
+ "uploadSuccess": "Загруска удолась.",
+ "exportError": "Экспорт не удолся.",
+ "exportSuccess": "Экспорт удолся.",
+ "importError": "Импорт не удолся.",
+ "importSuccess": "Импорт удолся.",
+ "tapHint": "Нажмите чтобы показать",
+ "inputError": "Не должно быть пустым.",
+ "inputHint": "Заглавие",
+ "info1": "Версиа",
+ "info2": "Разработлик",
+ "info3": "Сайт",
+ "info4": "Исходный код",
+ "info5": "Юристическое уведомление",
+ "settingTitle1": "Отображение",
+ "settingTitle2": "Управление",
+ "settingTitle3": "Даты",
+ "setting11": "Показывать фото",
+ "setting21": "Источник фото",
+ "setting22": "Настроики сервера",
+ "setting31": "Экспорт в фаил",
+ "setting32": "Импорт из фаила",
+ "option111": "Камера",
+ "option112": "Галерея",
+ "option211": "Всегда",
+ "option212": "В поиске",
+ "option213": "В списках",
+ "option214": "Только в рецептах",
+ "option311": "Экспорт",
+ "option312": "Импорт",
+ "infoField1": "Тип",
+ "infoField2": "Версиа",
+ "infoField3": "Размер",
+ "infoField4": "Внесение",
+ "legalease": "Это приложение является объектом авторских прав разработчика. Применяются ограничения, условия и разрешения лицензии «BSD 2 Clause». Лицензии третьих личностей прилагаются ниже.",
+ "noContentError": "Ешё пусто.",
+ "noNetworkError": "Не удалось скачать рецепты.",
+ "unknownError": "Что-то пошло не так.",
+ "unknown": "Неизвестна",
+ "removed": "Внесение удалёно",
+ "retry": "Повторить попытку",
+ "undo": "Назад"
+} \ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 0000000..b76dc5b
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,88 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import 'package:kulinar_app/constants.dart';
+import 'package:kulinar_app/models/data/shoplist_data_class.dart';
+import 'package:kulinar_app/views/main_view.dart';
+import 'package:kulinar_app/util/notifications.dart';
+import 'package:kulinar_app/models/data/recipe_data_class.dart';
+import 'package:kulinar_app/models/data/settings_data_class.dart';
+
+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();
+
+ const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings("@mipmap/notification_icon");
+ final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid);
+ await Notifications.flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: Notifications.selectNotification);
+
+ await RecipeData.load();
+ await SettingsData.load();
+ await ShoplistData.load();
+
+ runApp(AppRoot());
+}
+
+class AppRoot extends StatefulWidget {
+ const AppRoot({Key? key}) : super(key: key);
+
+ @override
+ _AppRootState createState() => _AppRootState();
+}
+
+class _AppRootState extends State<AppRoot> {
+ @override
+ Widget build(BuildContext context) {
+ SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
+ statusBarColor: Colors.transparent,
+ statusBarIconBrightness: Brightness.light,
+ ));
+
+ return MaterialApp(
+ localizationsDelegates: [
+ AppLocalizations.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: [
+ const Locale('de', ''),
+ const Locale('en', ''),
+ const Locale('ru', ''),
+ ],
+ theme: themeData(),
+ title: "Kulinar",
+ home: MainView(),
+ );
+ }
+
+ ThemeData themeData() {
+ return ThemeData(
+ // TODO: Experiment what this does
+ // brightness: Brightness.light,
+ primarySwatch: cPrimarySwatchColor,
+ textSelectionTheme: TextSelectionThemeData(
+ cursorColor: cIconColor,
+ ),
+ inputDecorationTheme: InputDecorationTheme(
+ hintStyle: cSearchTextStyle,
+ ),
+ appBarTheme: AppBarTheme(
+ actionsIconTheme: IconThemeData(color: cIconColor),
+ titleTextStyle: cTitleStyle,
+ toolbarTextStyle: cTitleStyle,
+ iconTheme: IconThemeData(color: cIconColor),
+ centerTitle: true,
+ elevation: 10.0,
+ ),
+ buttonTheme: ButtonThemeData(
+ buttonColor: cPrimaryColor,
+ ),
+ );
+ }
+}
diff --git a/lib/models/data/recipe_data_class.dart b/lib/models/data/recipe_data_class.dart
new file mode 100644
index 0000000..ebbc5e1
--- /dev/null
+++ b/lib/models/data/recipe_data_class.dart
@@ -0,0 +1,52 @@
+import 'dart:convert';
+
+import 'package:kulinar_app/models/recipe_class.dart';
+import 'package:kulinar_app/util/storage_handler.dart';
+
+class RecipeData {
+ // TODO: What is this?
+ static List<Recipe> remoteRecipeList = [];
+ static List<Recipe> recipeList = [];
+
+ static Future<void> save() async {
+ await StorageHandler.store("recipes", encode());
+ }
+
+ static Future<void> load() async {
+ decode(await StorageHandler.fetch("recipes") ?? "[]");
+ }
+
+ static String encode() {
+ List<Map<String, dynamic>> _tempList = [];
+
+ recipeList.forEach((element) {
+ Map<String, dynamic> _map = Map<String, dynamic>();
+
+ _map["title"] = element.title;
+ _map["description"] = element.description;
+ _map["favorite"] = element.favorite;
+ _map["rating"] = element.rating;
+ _map["image"] = element.image;
+
+ _tempList.add(_map);
+ });
+
+ return jsonEncode(_tempList);
+ }
+
+ static void decode(String data) {
+ final _result = jsonDecode(data);
+
+ if (_result.isEmpty) return;
+
+ _result.forEach((item) {
+ recipeList.add(Recipe(
+ title: item["title"],
+ description: item["description"],
+ favorite: item["favorite"],
+ rating: item["rating"],
+ image: item["image"],
+ ));
+ });
+ }
+}
diff --git a/lib/models/data/settings_data_class.dart b/lib/models/data/settings_data_class.dart
new file mode 100644
index 0000000..e8cd604
--- /dev/null
+++ b/lib/models/data/settings_data_class.dart
@@ -0,0 +1,33 @@
+import 'dart:convert';
+
+import 'package:kulinar_app/util/storage_handler.dart';
+
+class SettingsData {
+ static Map<String, String> settings = {
+ "showPhotos": "0",
+ "photoSource": "1",
+ "serverURL": "",
+ };
+
+ static Future<void> save() async {
+ await StorageHandler.store("settings", encode());
+ }
+
+ static Future<void> load() async {
+ decode(await StorageHandler.fetch("settings") ?? "{}");
+ }
+
+ static String encode() {
+ return jsonEncode(settings);
+ }
+
+ static void decode(String data) {
+ final _result = jsonDecode(data);
+
+ if (_result.isEmpty) return;
+
+ _result.forEach((key, value) {
+ settings[key] = value;
+ });
+ }
+}
diff --git a/lib/models/data/shoplist_data_class.dart b/lib/models/data/shoplist_data_class.dart
new file mode 100644
index 0000000..3956282
--- /dev/null
+++ b/lib/models/data/shoplist_data_class.dart
@@ -0,0 +1,31 @@
+import 'dart:convert';
+
+import 'package:kulinar_app/util/storage_handler.dart';
+
+class ShoplistData {
+ static List<String> shoplist = [];
+ static List<String> removed = [];
+
+ static Future<void> save() async {
+ print("asd");
+ await StorageHandler.store("shoplist", encode());
+ }
+
+ static Future<void> load() async {
+ decode(await StorageHandler.fetch("shoplist") ?? "[]");
+ }
+
+ static String encode() {
+ return jsonEncode(shoplist);
+ }
+
+ static void decode(String data) {
+ final _result = jsonDecode(data);
+
+ if (_result.isEmpty) return;
+
+ _result.forEach((item) {
+ shoplist.add(item);
+ });
+ }
+}
diff --git a/lib/models/recipe_class.dart b/lib/models/recipe_class.dart
new file mode 100644
index 0000000..6873a1f
--- /dev/null
+++ b/lib/models/recipe_class.dart
@@ -0,0 +1,63 @@
+import 'dart:convert';
+
+import 'package:kulinar_app/models/data/recipe_data_class.dart';
+
+class Recipe {
+ String? title;
+ String? image;
+ String? description;
+ bool favorite;
+ int rating;
+
+ Recipe({this.title, this.image, this.description, this.favorite = false, this.rating = 0});
+
+ bool isListed({bool remote = false}) {
+ List<Recipe> _list = remote ? RecipeData.remoteRecipeList : RecipeData.recipeList;
+
+ for (Recipe recipe in _list) {
+ if (this.title == recipe.title && this.image == recipe.image && this.description == recipe.description) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ bool isDefault() {
+ if (this.title != null) return false;
+ if (this.image != null) return false;
+ if (this.description != null) return false;
+ if (this.favorite != false) return false;
+ if (this.rating != 0) return false;
+
+ return true;
+ }
+
+ static Recipe fromJson(String string) {
+ final json = jsonDecode(string);
+
+ return Recipe(title: json["title"], image: json["image"], description: json["description"]);
+ }
+
+ String toJsonString() {
+ Map<String, String> map = {
+ "title": this.title ?? "",
+ "image": this.image ?? "",
+ "description": this.description ?? "",
+ };
+
+ return jsonEncode(map);
+ }
+
+ void toggleFavorite() {
+ this.favorite = !this.favorite;
+ }
+
+ void updateRating() {
+ if (this.rating == 5) {
+ this.rating = 0;
+ } else {
+ this.rating++;
+ }
+ }
+}
diff --git a/lib/util/file_handler.dart b/lib/util/file_handler.dart
new file mode 100644
index 0000000..0344c60
--- /dev/null
+++ b/lib/util/file_handler.dart
@@ -0,0 +1,109 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+
+import 'package:kulinar_app/constants.dart';
+import 'package:kulinar_app/util/notifications.dart';
+import 'package:kulinar_app/widgets/toastbar_widget.dart';
+import 'package:kulinar_app/models/data/recipe_data_class.dart';
+import 'package:kulinar_app/models/data/settings_data_class.dart';
+
+import 'package:file_picker/file_picker.dart';
+import 'package:external_path/external_path.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+enum ExportType {
+ settings,
+ recipes,
+ recipe,
+ full,
+}
+
+/// Handles all the things related to file serialization / deserialization and other file interaction.
+class FileHandler {
+ /// Serializes the given `data` into a json style File. Sends a confirmation Toast to the given `context` afterwards.
+ static Future<void> serializeFile(BuildContext context, Map<String, dynamic> data) async {
+ if (await Permission.storage.request().isGranted) {
+ try {
+ final _directory = await ExternalPath.getExternalStoragePublicDirectory(ExternalPath.DIRECTORY_DOWNLOADS);
+ final _file = File("$_directory/Export-${DateTime.now().toUtc().toString().split(" ")[0]}.kulinar");
+
+ await _file.writeAsString(jsonEncode(await encodeDataAsMap(ExportType.full, data)));
+
+ ToastBar.showToastBar(context, AppLocalizations.of(context)!.exportSuccess);
+ Notifications.notify(AppLocalizations.of(context)!.exportSuccess, AppLocalizations.of(context)!.tapHint, _file.path);
+ } catch (e) {
+ print(e);
+ ToastBar.showToastBar(context, AppLocalizations.of(context)!.exportError);
+ }
+ }
+ }
+
+ /// Deserializes the given `file` into the returned Map. Sends a confirmation Toast to the given `context` afterwards.
+ static Future<Map<String, String>> deserializeFile(BuildContext context, File file) async {
+ final dynamic _content = jsonDecode(await file.readAsString());
+
+ SettingsData.decode(_content["settings"]);
+ RecipeData.decode(_content["recipes"]);
+
+ ToastBar.showToastBar(context, AppLocalizations.of(context)!.importSuccess);
+
+ return _content;
+ }
+
+ /// Parses a given possible deserializable `file` for useful information and returns a map of it.
+ static Future<Map<String, String>> deserializeFileInformation(File file) async {
+ final Map<dynamic, dynamic> _content = jsonDecode(await file.readAsString());
+ Map<String, String> _map = Map<String, String>();
+
+ _map["version"] = _content["version"];
+ _map["type"] = _content["type"].split(".")[1];
+ _map["size"] = "${await file.length()} Bytes";
+
+ if (_content["type"] == ExportType.full.toString()) {
+ int a = jsonDecode(_content["recipes"]).length;
+ int b = jsonDecode(_content["settings"]).length;
+
+ _map["entries"] = (a + b).toString();
+ }
+
+ return _map;
+ }
+
+ /// Opens the native file picker and returns the picked `.kulinar` file.
+ static Future<File?> pickDeserializableFile(BuildContext context) async {
+ if (await Permission.storage.request().isGranted) {
+ try {
+ FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ["kulinar"]);
+
+ if (result != null) {
+ return File(result.files.single.path!);
+ }
+ } catch (e) {
+ print(e);
+ ToastBar.showToastBar(context, AppLocalizations.of(context)!.importError);
+ }
+ }
+
+ return null;
+ }
+
+ /// Encodes the given `data` together with some additional information into a serializable map.
+ static Future<Map<String, String>> encodeDataAsMap(ExportType type, Map<String, dynamic> data) async {
+ Map<String, String> _map = Map<String, String>();
+
+ _map["version"] = cVersion;
+ _map["type"] = type.toString();
+ // TODO: IMPLEMENT: Base64 Images and image count on export
+ // map["imageCount"] = getImageCount;
+ _map.addAll(data as Map<String, String>);
+
+ return _map;
+ }
+
+ /// Decodes the given `map` into
+ /// TODO: IMPLEMENT: decodeExportMap
+ // static Future<Map<String, String>> decodeMapToData(ExportType type) async {}
+}
diff --git a/lib/util/notifications.dart b/lib/util/notifications.dart
new file mode 100644
index 0000000..91280da
--- /dev/null
+++ b/lib/util/notifications.dart
@@ -0,0 +1,32 @@
+import 'package:open_file/open_file.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+
+/// Handles all the notification related things.
+class Notifications {
+ static FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
+
+ /// Specifies the exact notification behavior on android.
+ static const AndroidNotificationDetails _androidSpecifics = AndroidNotificationDetails(
+ 'com.davidpenkowoj.kulinar',
+ 'Kulinar',
+ importance: Importance.defaultImportance,
+ priority: Priority.defaultPriority,
+ autoCancel: true,
+ playSound: false,
+ showWhen: false,
+ );
+
+ /// Is called when the notification is pressed.
+ static Future<dynamic> selectNotification(String? payload) async {
+ if (payload != null) {
+ OpenFile.open(payload, type: "*/*"); // TODO: FIXME: Still doesnt work (sometimes)
+ }
+ }
+
+ /// Launches the notification
+ static Future<void> notify(String title, String description, String payload) async {
+ const NotificationDetails _platformChannelSpecifics = NotificationDetails(android: _androidSpecifics);
+
+ await flutterLocalNotificationsPlugin.show(0, title, description, _platformChannelSpecifics, payload: payload);
+ }
+}
diff --git a/lib/util/storage_handler.dart b/lib/util/storage_handler.dart
new file mode 100644
index 0000000..1e12ed7
--- /dev/null
+++ b/lib/util/storage_handler.dart
@@ -0,0 +1,20 @@
+import 'package:shared_preferences/shared_preferences.dart';
+
+/// Handles all persistance related app storage
+class StorageHandler {
+ /// Stores the given `data` as a string with the `key`.
+ static Future<String> store(String key, String data) async {
+ final _prefs = await SharedPreferences.getInstance();
+
+ _prefs.setString(key, data);
+
+ return data;
+ }
+
+ /// Fetches the string related to the given `key`.
+ static Future<String?> fetch(String key) async {
+ final _prefs = await SharedPreferences.getInstance();
+
+ return _prefs.getString(key);
+ }
+}
diff --git a/lib/views/favorites_view.dart b/lib/views/favorites_view.dart
new file mode 100644
index 0000000..bb0f7fb
--- /dev/null
+++ b/lib/views/favorites_view.dart
@@ -0,0 +1,82 @@
+import 'package:flutter/material.dart';
+
+import 'package:kulinar_app/models/recipe_class.dart';
+import 'package:kulinar_app/widgets/error_widgets.dart';
+import 'package:kulinar_app/widgets/toastbar_widget.dart';
+import 'package:kulinar_app/widgets/recipe_card_widget.dart';
+import 'package:kulinar_app/widgets/custom_drawer_widget.dart';
+import 'package:kulinar_app/models/data/recipe_data_class.dart';
+import 'package:kulinar_app/widgets/recipe_search_delegate.dart';
+
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class FavoritesView extends StatefulWidget {
+ const FavoritesView({Key? key}) : super(key: key);
+
+ @override
+ _FavoritesViewState createState() => _FavoritesViewState();
+}
+
+class _FavoritesViewState extends State<FavoritesView> {
+ BuildContext? _toastyContext;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: _buildAppBar(context),
+ drawer: CustomDrawer(initalIndex: 1),
+ body: Container(
+ width: double.infinity,
+ child: Builder(
+ builder: (BuildContext context) {
+ _toastyContext = context;
+
+ return _buildListView();
+ },
+ ),
+ ),
+ );
+ }
+
+ void redrawFavoritesView() {
+ setState(() {});
+ }
+
+ void showToastCallback(String content, String actionLabel, Function actionCallback) {
+ ToastBar.showToastBar(_toastyContext!, content, actionLabel: actionLabel, actionCallback: actionCallback);
+ }
+
+ PreferredSizeWidget _buildAppBar(BuildContext context) {
+ return AppBar(
+ title: Text(AppLocalizations.of(context)!.category4),
+ actions: [
+ IconButton(
+ icon: Icon(Icons.search),
+ onPressed: () async {
+ await showSearch(
+ context: context,
+ delegate: FavoriteRecipeSearch(),
+ );
+
+ setState(() {});
+ },
+ ),
+ ],
+ );
+ }
+
+ Widget _buildListView() {
+ List<Recipe> _filteredRecipeList = RecipeData.recipeList.where((element) => element.favorite).toList();
+
+ if (_filteredRecipeList.isEmpty) return NoContentError();
+
+ return ListView.builder(
+ itemCount: _filteredRecipeList.length,
+ itemBuilder: (context, index) => RecipeCard(
+ recipe: _filteredRecipeList[index],
+ redrawCallback: redrawFavoritesView,
+ showToastCallback: showToastCallback,
+ ),
+ );
+ }
+}
diff --git a/lib/views/file_info.dart b/lib/views/file_info.dart
new file mode 100644
index 0000000..e032716
--- /dev/null
+++ b/lib/views/file_info.dart
@@ -0,0 +1,103 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+
+import 'package:kulinar_app/constants.dart';
+import 'package:kulinar_app/util/file_handler.dart';
+import 'package:kulinar_app/widgets/error_widgets.dart';
+
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class FileInfo extends StatefulWidget {
+ FileInfo({Key? key, required this.filePath}) : super(key: key);
+
+ final String filePath;
+
+ @override
+ _FileInfoState createState() => _FileInfoState();
+}
+
+class _FileInfoState extends State<FileInfo> {
+ @override
+ Widget build(BuildContext context) {
+ final File _file = File(widget.filePath.split(":")[1]);
+
+ return Scaffold(
+ appBar: AppBar(title: Text(AppLocalizations.of(context)!.category9)),
+ body: FutureBuilder(
+ future: FileHandler.deserializeFileInformation(_file),
+ builder: (BuildContext context, AsyncSnapshot<Map<String, String>> snapshot) {
+ if (snapshot.connectionState != ConnectionState.done) return CircularProgressIndicator();
+ if (snapshot.hasError || !snapshot.hasData) return UnknownError();
+
+ return _buildFileInfoTable(_getSortedSnapshotData(snapshot), _file);
+ },
+ ),
+ );
+ }
+
+ String _localizeInfoField(BuildContext context, String field) {
+ switch (field) {
+ case "type":
+ return AppLocalizations.of(context)!.infoField1;
+ case "version":
+ return AppLocalizations.of(context)!.infoField2;
+ case "size":
+ return AppLocalizations.of(context)!.infoField3;
+ case "entries":
+ return AppLocalizations.of(context)!.infoField4;
+ default:
+ return AppLocalizations.of(context)!.unknown;
+ }
+ }
+
+ List<TableRow> _getSortedSnapshotData(AsyncSnapshot<Map<String, String>> snapshot) {
+ List<TableRow> _children = [];
+
+ snapshot.data!.forEach((key, value) {
+ _children.add(
+ TableRow(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(right: 4.0),
+ child: Text(_localizeInfoField(context, key), style: cTableKeyStyle, textAlign: TextAlign.right),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 4.0),
+ child: Text(value, style: cTableValueStyle),
+ ),
+ ],
+ ),
+ );
+ });
+
+ return _children;
+ }
+
+ Widget _buildFileInfoTable(List<TableRow> children, File file) {
+ return Padding(
+ padding: const EdgeInsets.only(top: 16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Table(
+ columnWidths: const <int, TableColumnWidth>{
+ 0: FlexColumnWidth(),
+ 1: FlexColumnWidth(),
+ },
+ children: children,
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: ElevatedButton(
+ child: Text(AppLocalizations.of(context)!.option312, style: cOptionTextStyle.copyWith(color: cIconColor)),
+ onPressed: () {
+ FileHandler.deserializeFile(context, file);
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/views/image_view.dart b/lib/views/image_view.dart
new file mode 100644
index 0000000..8721d5e
--- /dev/null
+++ b/lib/views/image_view.dart
@@ -0,0 +1,39 @@
+import 'dart:io';
+
+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);
+
+ final String image;
+
+ @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)),
+ ),
+ ),
+ onTap: () {
+ Navigator.pop(context);
+ },
+ ),
+ ),
+ ),
+ );
+ }
+
+ // TODO: IMPLEMENT: average color background
+ Color _averageImageColor(String path) {
+ return cPrimaryColor;
+ }
+}
diff --git a/lib/views/info_view.dart b/lib/views/info_view.dart
new file mode 100644
index 0000000..e2faeab
--- /dev/null
+++ b/lib/views/info_view.dart
@@ -0,0 +1,89 @@
+import 'package:flutter/material.dart';
+
+import 'package:kulinar_app/constants.dart';
+import 'package:kulinar_app/widgets/custom_drawer_widget.dart';
+
+import 'package:url_launcher/url_launcher.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class InfoView extends StatefulWidget {
+ const InfoView({Key? key}) : super(key: key);
+
+ @override
+ _InfoViewState createState() => _InfoViewState();
+}
+
+class _InfoViewState extends State<InfoView> {
+ String websiteUrl = "https://davidpkj.github.io/kulinar_app.html";
+ String sourceCodeUrl = "https://github.com/davidpkj/kulinar_app";
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: Text(AppLocalizations.of(context)!.category9)),
+ drawer: CustomDrawer(initalIndex: 6),
+ body: ListView(
+ children: <Widget>[
+ Padding(
+ padding: const EdgeInsets.only(top: 15.0, left: 15.0),
+ child: Text(AppLocalizations.of(context)!.category9, style: cTinyTitleStyle),
+ ),
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.info1, style: cDefaultTextStyle),
+ subtitle: Text(cVersion, style: cDetailsTextStyle),
+ ),
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.info2, style: cDefaultTextStyle),
+ subtitle: Text("David Penkowoj", style: cDetailsTextStyle),
+ ),
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.info3, style: cDefaultTextStyle),
+ subtitle: Text(websiteUrl, style: cDetailsTextStyle, overflow: TextOverflow.ellipsis),
+ trailing: IconButton(
+ icon: Icon(Icons.north_east_rounded),
+ onPressed: () {
+ _launchURL(Uri.parse(websiteUrl));
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.info4, style: cDefaultTextStyle),
+ subtitle: Text(sourceCodeUrl, style: cDetailsTextStyle, overflow: TextOverflow.ellipsis),
+ trailing: IconButton(
+ icon: Icon(Icons.north_east_rounded),
+ onPressed: () {
+ _launchURL(Uri.parse(sourceCodeUrl));
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.info5, style: cDefaultTextStyle),
+ trailing: IconButton(
+ icon: Icon(Icons.launch_rounded),
+ onPressed: () {
+ _showDialog();
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ _showDialog() {
+ showAboutDialog(
+ context: context,
+ applicationVersion: cVersion,
+ applicationIcon: SizedBox(height: 35, width: 35, child: Image.asset("assets/icons/icon.png")),
+ applicationLegalese: AppLocalizations.of(context)!.legalease,
+ );
+ }
+
+ _launchURL(Uri url) async {
+ if (await canLaunchUrl(url)) {
+ await launchUrl(url);
+ } else {
+ throw 'Could not launch $url';
+ }
+ }
+}
diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart
new file mode 100644
index 0000000..678db4a
--- /dev/null
+++ b/lib/views/main_view.dart
@@ -0,0 +1,141 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import 'package:kulinar_app/constants.dart';
+import 'package:kulinar_app/views/file_info.dart';
+import 'package:kulinar_app/views/recipe_view.dart';
+import 'package:kulinar_app/models/recipe_class.dart';
+import 'package:kulinar_app/widgets/error_widgets.dart';
+import 'package:kulinar_app/widgets/toastbar_widget.dart';
+import 'package:kulinar_app/widgets/recipe_card_widget.dart';
+import 'package:kulinar_app/widgets/custom_drawer_widget.dart';
+import 'package:kulinar_app/models/data/recipe_data_class.dart';
+import 'package:kulinar_app/widgets/recipe_search_delegate.dart';
+import 'package:kulinar_app/widgets/page_route_transitions.dart';
+
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class MainView extends StatefulWidget {
+ const MainView({Key? key}) : super(key: key);
+
+ @override
+ _MainViewState createState() => _MainViewState();
+}
+
+class _MainViewState extends State<MainView> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
+ static const platform = const MethodChannel("com.davidpenkowoj.kulinar.openfile");
+
+ TabController? _tabController;
+ BuildContext? _toastyContext;
+
+ @override
+ void initState() {
+ super.initState();
+ getOpenFileUrl();
+ WidgetsBinding.instance.addObserver(this);
+ _tabController = TabController(length: 2, vsync: this);
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ _tabController!.dispose();
+ WidgetsBinding.instance.removeObserver(this);
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (state == AppLifecycleState.resumed) {
+ getOpenFileUrl();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: _buildAppBar(context, _tabController!),
+ drawer: CustomDrawer(initalIndex: 0),
+ floatingActionButton: _buildFloatingActionButton(),
+ body: Builder(builder: (BuildContext context) {
+ _toastyContext = context;
+
+ return TabBarView(
+ controller: _tabController,
+ children: [
+ _buildListView(RecipeData.recipeList.where((element) => element.rating > 0).toList()),
+ _buildListView(RecipeData.recipeList.where((element) => element.rating == 0).toList()),
+ ],
+ );
+ }),
+ );
+ }
+
+ void getOpenFileUrl() async {
+ dynamic url = await platform.invokeMethod("getOpenFileUrl");
+
+ if (url != null) {
+ setState(() {
+ Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (BuildContext context) => FileInfo(filePath: url)), (_) => false);
+ });
+ }
+ }
+
+ void showToastCallback(String content, String actionLabel, Function actionCallback) {
+ ToastBar.showToastBar(_toastyContext!, content, actionLabel: actionLabel, actionCallback: actionCallback);
+ }
+
+ Widget _buildFloatingActionButton() {
+ return FloatingActionButton(
+ child: Icon(Icons.add, color: cIconColor),
+ onPressed: () async {
+ await Navigator.push(
+ context,
+ SlideFromBottomRoute(child: RecipeView(readonly: false)),
+ );
+
+ setState(() {});
+ },
+ );
+ }
+
+ Widget _buildListView(List<Recipe> filteredRecipeList) {
+ if (filteredRecipeList.isEmpty) return NoContentError();
+
+ return ListView.builder(
+ itemCount: filteredRecipeList.length,
+ itemBuilder: (context, index) => RecipeCard(recipe: filteredRecipeList[index], redrawCallback: redrawMainView, showToastCallback: showToastCallback),
+ );
+ }
+
+ PreferredSizeWidget _buildAppBar(BuildContext context, TabController tabController) {
+ return AppBar(
+ title: Text(AppLocalizations.of(context)!.category1),
+ actions: [
+ IconButton(
+ icon: Icon(
+ Icons.search,
+ ),
+ onPressed: () async {
+ await showSearch(
+ context: context,
+ delegate: RecipeSearch(),
+ );
+
+ setState(() {});
+ },
+ ),
+ ],
+ bottom: TabBar(
+ controller: tabController,
+ tabs: [
+ Tab(child: Text(AppLocalizations.of(context)!.category2, style: cSubTitleStyle)),
+ Tab(child: Text(AppLocalizations.of(context)!.category3, style: cSubTitleStyle)),
+ ],
+ ),
+ );
+ }
+
+ Future<void> redrawMainView() async {
+ setState(() {});
+ }
+}
diff --git a/lib/views/recipe_view.dart b/lib/views/recipe_view.dart
new file mode 100644
index 0000000..960ba84
--- /dev/null
+++ b/lib/views/recipe_view.dart
@@ -0,0 +1,379 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:http/http.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/settings_data_class.dart';
+
+import 'package:image_picker/image_picker.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:share_plus/share_plus.dart';
+
+class RecipeView extends StatefulWidget {
+ const RecipeView({Key? key, this.remote = false, required this.readonly, this.recipe, this.redrawCallback, this.showToastCallback}) : super(key: key);
+
+ final bool remote;
+ final bool readonly;
+ final Recipe? recipe;
+ final Function? redrawCallback;
+ final Function? showToastCallback;
+
+ @override
+ _RecipeViewState createState() => _RecipeViewState();
+}
+
+class _RecipeViewState extends State<RecipeView> {
+ final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+ final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
+ final TextEditingController _controller1 = TextEditingController();
+ final TextEditingController _controller2 = TextEditingController();
+ final FocusNode _detailsTitleFocus = FocusNode();
+ final FocusNode _detailsTextFocus = FocusNode();
+ Recipe _unsavedRecipe = Recipe();
+ bool _readonlyOverride = 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();
+ } 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 (!widget.readonly) {
+ RecipeData.recipeList.add(_unsavedRecipe);
+ RecipeData.save();
+ }
+
+ if (widget.recipe == null) {
+ Navigator.pop(context);
+ } else {
+ _readonlyOverride = false;
+
+ setState(() {});
+ }
+ }
+
+ void _removeRecipe() {
+ 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;
+
+ // FIXME: This might throw due to "!"
+ widget.showToastCallback!(_content, _actionLabel, () {
+ _addRecipe(passedRecipe: _removedRecipe);
+ widget.redrawCallback!();
+ });
+
+ setState(() {});
+ }
+
+ bool _isEditingAllowed() {
+ if (_readonlyOverride) return true;
+ if (!widget.readonly) return true;
+
+ return false;
+ }
+
+ // 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(() {});
+ }
+
+ // 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);
+ }
+
+ 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(() {});
+ }
+
+ // FIXME: doesnt refresh, should be passed from parent
+ 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,
+ readonly: !_isEditingAllowed(),
+ unsavedRecipe: _unsavedRecipe,
+ pickImageCallback: _pickImage,
+ removeImageCallback: _removeImage,
+ removeRecipeCallback: _removeRecipe,
+ downloadRecipeCallback: downloadRecipe,
+ isEditingAllowedCallback: _isEditingAllowed,
+ cacheUnsavedRecipeCallback: _cacheUnsavedRecipe,
+ );
+ }
+ }
+
+ Widget _buildTitleInput() {
+ return Padding(
+ padding: const EdgeInsets.only(right: 20.0, left: 20.0),
+ child: TextFormField(
+ controller: _controller1,
+ style: cRecipeTextStyle,
+ cursorColor: cPrimaryColor,
+ enabled: _isEditingAllowed(),
+ 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) {
+ // 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(
+ 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: 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,
+ ),
+ ),
+ );
+ }
+
+ void _shareData() {
+ Share.share("${_unsavedRecipe.title}\n\n${_unsavedRecipe.description}", subject: _unsavedRecipe.title);
+ }
+
+ void _addToShoppingList() {}
+
+ void _uploadRecipe(BuildContext context) async {
+ Map<String, String> _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<Widget> _buildAppBarActions(BuildContext context, Recipe recipe) {
+ final _actionList = [_shareData, _addToShoppingList, _uploadRecipe, _removeRecipe];
+
+ List<Widget> _localActions = [
+ _buildAppBarCheckActions(),
+ PopupMenuButton(
+ onSelected: (int value) => _actionList.elementAt(value)(),
+ itemBuilder: (BuildContext context) => [
+ // TODO: Translations
+ PopupMenuItem<int>(value: 0, child: Text("Teilen")),
+ PopupMenuItem<int>(value: 1, child: Text("Einkaufen")),
+ PopupMenuItem<int>(value: 2, child: Text("Hochladen")),
+ PopupMenuItem<int>(value: 3, child: Text("Löschen")),
+ ],
+ ),
+ ];
+
+ List<Widget> _remoteDownloadAction = [
+ IconButton(
+ icon: Icon(Icons.save_alt_rounded),
+ onPressed: () {
+ downloadRecipe(context, recipe);
+ },
+ ),
+ ];
+
+ List<Widget> _remoteLookupAction = [
+ IconButton(
+ icon: Icon(Icons.menu_open_rounded),
+ onPressed: null, // TODO: IMPLEMENT: Show local recipe
+ ),
+ ];
+
+ 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 (_isEditingAllowed()) _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 (_isEditingAllowed()) {
+ return IconButton(
+ icon: Icon(Icons.check),
+ onPressed: _addRecipe,
+ );
+ } else {
+ return IconButton(
+ icon: Icon(Icons.edit),
+ onPressed: () {
+ _readonlyOverride = true;
+
+ setState(() {});
+ },
+ );
+ }
+ }
+}
diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart
new file mode 100644
index 0000000..7f2ae90
--- /dev/null
+++ b/lib/views/settings_view.dart
@@ -0,0 +1,184 @@
+import 'package:flutter/material.dart';
+
+import 'package:kulinar_app/constants.dart';
+import 'package:kulinar_app/util/file_handler.dart';
+import 'package:kulinar_app/widgets/custom_drawer_widget.dart';
+import 'package:kulinar_app/models/data/recipe_data_class.dart';
+import 'package:kulinar_app/models/data/settings_data_class.dart';
+
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class SettingsView extends StatefulWidget {
+ const SettingsView({Key? key}) : super(key: key);
+
+ @override
+ _SettingsViewState createState() => _SettingsViewState();
+}
+
+// TODO: Add subtitles
+class _SettingsViewState extends State<SettingsView> {
+ final TextEditingController _controller = TextEditingController();
+ final FocusNode _focusNode = FocusNode();
+
+ @override
+ void initState() {
+ super.initState();
+ _controller.text = SettingsData.settings["serverURL"]!;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: Text(AppLocalizations.of(context)!.category8)),
+ drawer: CustomDrawer(initalIndex: 5),
+ body: Builder(
+ builder: (BuildContext context) => ListView(
+ children: [
+ _buildTinyTitle(AppLocalizations.of(context)!.settingTitle1),
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.setting11, style: cDefaultTextStyle),
+ trailing: _buildShowPhotosDropDownButton(),
+ ),
+ _buildTinyTitle(AppLocalizations.of(context)!.settingTitle2),
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.setting21, style: cDefaultTextStyle),
+ trailing: _buildPhotoSourceDropDownButton(),
+ ),
+ /*
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.setting22, style: cDefaultTextStyle),
+ trailing: _buildServerIPInput(),
+ ),
+ */
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.setting22, style: cDefaultTextStyle),
+ trailing: IconButton(
+ icon: Icon(Icons.arrow_forward_rounded),
+ onPressed: null, // TODO: IMPLEMENT: specific server settings
+ ),
+ ),
+ _buildTinyTitle(AppLocalizations.of(context)!.settingTitle3),
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.setting31, style: cDefaultTextStyle),
+ trailing: ElevatedButton(
+ child: Text(AppLocalizations.of(context)!.option311, style: cOptionTextStyle.copyWith(color: cIconColor)),
+ onPressed: () async {
+ final data = {
+ "recipes": RecipeData.encode(),
+ "settings": SettingsData.encode(),
+ };
+
+ await FileHandler.serializeFile(context, data);
+
+ setState(() {});
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(AppLocalizations.of(context)!.setting32, style: cDefaultTextStyle),
+ trailing: ElevatedButton(
+ child: Text(AppLocalizations.of(context)!.option312, style: cOptionTextStyle.copyWith(color: cIconColor)),
+ onPressed: () async {
+ final _file = await FileHandler.pickDeserializableFile(context);
+
+ await FileHandler.deserializeFile(context, _file!);
+
+ setState(() {});
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _updateSettings(String text) {
+ _focusNode.unfocus();
+
+ if (text != null) {
+ SettingsData.settings["serverURL"] = text;
+ SettingsData.save();
+ }
+ }
+
+/*
+ // TODO: Use later in direct settings?
+ Widget _buildServerIPInput() {
+ if (MediaQuery.of(context).viewInsets.bottom == 0) _updateSettings(null);
+
+ return Container(
+ width: 150.0,
+ child: TextFormField(
+ keyboardType: TextInputType.url,
+ controller: _controller,
+ style: cRecipeTextStyle,
+ focusNode: _focusNode,
+ cursorColor: cPrimaryColor,
+ onEditingComplete: () {
+ _updateSettings(_controller.text);
+ },
+ ),
+ );
+ }
+*/
+
+ Widget _buildPhotoSourceDropDownButton() {
+ return DropdownButton(
+ value: int.parse(SettingsData.settings["photoSource"]!),
+ items: [
+ DropdownMenuItem(
+ child: Text(AppLocalizations.of(context)!.option111, style: cOptionTextStyle),
+ value: 0,
+ ),
+ DropdownMenuItem(
+ child: Text(AppLocalizations.of(context)!.option112, style: cOptionTextStyle),
+ value: 1,
+ ),
+ ],
+ onChanged: (value) {
+ SettingsData.settings["photoSource"] = value.toString();
+ SettingsData.save();
+
+ setState(() {});
+ },
+ );
+ }
+
+ Widget _buildShowPhotosDropDownButton() {
+ return DropdownButton(
+ value: int.parse(SettingsData.settings["showPhotos"]!),
+ items: [
+ DropdownMenuItem(
+ child: Text(AppLocalizations.of(context)!.option211, style: cOptionTextStyle),
+ value: 0,
+ ),
+ DropdownMenuItem(
+ child: Text(AppLocalizations.of(context)!.option212, style: cOptionTextStyle),
+ value: 1,
+ ),
+ DropdownMenuItem(
+ child: Text(AppLocalizations.of(context)!.option213, style: cOptionTextStyle),
+ value: 2,
+ ),
+ DropdownMenuItem(
+ child: Text(AppLocalizations.of(context)!.option214, style: cOptionTextStyle),
+ value: 3,
+ ),
+ ],
+ onChanged: (value) {
+ SettingsData.settings["showPhotos"] = value.toString();
+ SettingsData.save();
+
+ setState(() {});
+ },
+ );
+ }
+
+ Widget _buildTinyTitle(String title) {
+ return Padding(
+ padding: const EdgeInsets.only(top: 15.0, left: 15.0),
+ child: Text(title, style: cTinyTitleStyle),
+ );
+ }
+}
diff --git a/lib/views/shoplist_view.dart b/lib/views/shoplist_view.dart
new file mode 100644
index 0000000..a9caed8
--- /dev/null
+++ b/lib/views/shoplist_view.dart
@@ -0,0 +1,167 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+
+import 'package:kulinar_app/constants.dart';
+import 'package:kulinar_app/widgets/error_widgets.dart';
+import 'package:kulinar_app/widgets/toastbar_widget.dart';
+import 'package:kulinar_app/widgets/custom_drawer_widget.dart';
+import 'package:kulinar_app/models/data/shoplist_data_class.dart';
+
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class ShoplistView extends StatefulWidget {
+ const ShoplistView({Key? key}) : super(key: key);
+
+ @override
+ _ShoplistView createState() => _ShoplistView();
+}
+
+class _ShoplistView extends State<ShoplistView> {
+ final FocusNode _focusNode = FocusNode();
+ final TextEditingController _controller = TextEditingController();
+ final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
+ final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
+ PersistentBottomSheetController? _bottomSheetController;
+ bool _open = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ key: _scaffoldKey,
+ appBar: AppBar(title: Text(AppLocalizations.of(context)!.category7)),
+ drawer: CustomDrawer(initalIndex: 4),
+ floatingActionButton: _buildFloatingActionButton(),
+ body: _buildAnimatedList(),
+ );
+ }
+
+ Widget _buildFloatingActionButton() {
+ return FloatingActionButton(
+ child: Icon(_open ? Icons.remove_rounded : Icons.add_rounded, color: cIconColor),
+ onPressed: () {
+ if (_open) {
+ _closeInput();
+ } else {
+ _bottomSheetController = _scaffoldKey.currentState!.showBottomSheet((context) => _buildBottomSheetInput());
+ _open = !_open;
+
+ setState(() {});
+ }
+ },
+ );
+ }
+
+ // TODO: FIXME: Sometimes exception "cant call insertItem of null" is thrown when first item is added or last is removed
+ Widget _buildAnimatedList() {
+ if (ShoplistData.shoplist.length == 0) return NoContentError();
+
+ return AnimatedList(
+ key: _listKey,
+ initialItemCount: ShoplistData.shoplist.length,
+ itemBuilder: (BuildContext context, int index, Animation<double> animation) {
+ return SlideTransition(
+ position: animation.drive(Tween(begin: Offset(1.0, 0.0), end: Offset(0.0, 0.0))),
+ child: Dismissible(
+ key: Key(_getUniqueKeyString(ShoplistData.shoplist[index])),
+ background: _buildDimissibleBackground(true),
+ secondaryBackground: _buildDimissibleBackground(false),
+ child: ListTile(
+ title: Text(ShoplistData.shoplist[index]),
+ ),
+ onDismissed: (_) {
+ String _content = AppLocalizations.of(context)!.removed;
+ String _actionLabel = AppLocalizations.of(context)!.undo;
+
+ _removeItem(index);
+ ToastBar.showToastBar(context, _content, actionLabel: _actionLabel, actionCallback: () => _addItem(ShoplistData.removed.removeLast()));
+ },
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildDimissibleBackground(bool left) {
+ return Container(
+ color: cPrimaryColor,
+ /*
+ child: Row(
+ mainAxisAlignment: left ? MainAxisAlignment.start : MainAxisAlignment.end,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Icon(
+ Icons.delete_rounded,
+ color: cIconColor,
+ ),
+ ),
+ ],
+ ),
+ */
+ );
+ }
+
+ Widget _buildBottomSheetInput() {
+ _focusNode.requestFocus();
+
+ return WillPopScope(
+ child: Container(
+ height: 75.0,
+ color: cPrimaryColor,
+ child: Padding(
+ padding: const EdgeInsets.all(15.0),
+ child: TextFormField(
+ controller: _controller,
+ style: cRecipeTextStyle.copyWith(color: cIconColor),
+ cursorColor: cIconColor,
+ focusNode: _focusNode,
+ textInputAction: TextInputAction.done,
+ onEditingComplete: () {
+ String _text = _controller.text.trim();
+
+ _closeInput();
+ _controller.clear();
+
+ if (_text != "") _addItem(_text);
+ },
+ ),
+ ),
+ ),
+ onWillPop: () async {
+ return false;
+ },
+ );
+ }
+
+ String _getUniqueKeyString(String item) {
+ return base64Encode(utf8.encode(item));
+ }
+
+ void _removeItem(int index) {
+ _listKey.currentState!.removeItem(index, (_, __) => Container());
+ ShoplistData.removed.add(ShoplistData.shoplist.removeAt(index));
+
+ ShoplistData.save();
+
+ setState(() {});
+ }
+
+ void _addItem(String string) {
+ ShoplistData.shoplist.add(string);
+ if (ShoplistData.shoplist.length > 1) _listKey.currentState!.insertItem(ShoplistData.shoplist.indexOf(string));
+
+ ShoplistData.save();
+
+ setState(() {});
+ }
+
+ void _closeInput() {
+ _bottomSheetController!.close();
+ _focusNode.unfocus();
+
+ _open = !_open;
+
+ setState(() {});
+ }
+}
diff --git a/lib/views/vote_view.dart b/lib/views/vote_view.dart
new file mode 100644
index 0000000..7de2d10
--- /dev/null
+++ b/lib/views/vote_view.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+
+import 'package:kulinar_app/widgets/error_widgets.dart';
+import 'package:kulinar_app/widgets/custom_drawer_widget.dart';
+
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class VoteView extends StatefulWidget {
+ const VoteView({Key? key}) : super(key: key);
+
+ @override
+ _VoteViewState createState() => _VoteViewState();
+}
+
+// TODO: Change from vote to schedule
+class _VoteViewState extends State<VoteView> {
+ @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
+ );
+ }
+}
diff --git a/lib/views/week_view.dart b/lib/views/week_view.dart
new file mode 100644
index 0000000..8ea43a4
--- /dev/null
+++ b/lib/views/week_view.dart
@@ -0,0 +1,81 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+
+import 'package:kulinar_app/models/recipe_class.dart';
+import 'package:kulinar_app/widgets/error_widgets.dart';
+import 'package:kulinar_app/widgets/recipe_card_widget.dart';
+import 'package:kulinar_app/widgets/custom_drawer_widget.dart';
+import 'package:kulinar_app/models/data/recipe_data_class.dart';
+import 'package:kulinar_app/models/data/settings_data_class.dart';
+
+import 'package:http/http.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class WeekView extends StatefulWidget {
+ const WeekView({Key? key}) : super(key: key);
+
+ @override
+ _WeekViewState createState() => _WeekViewState();
+}
+
+class _WeekViewState extends State<WeekView> {
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: Text(AppLocalizations.of(context)!.category5)),
+ drawer: CustomDrawer(initalIndex: 2),
+ body: FutureBuilder(
+ future: _getWeeklyRecipes(),
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ if (snapshot.connectionState != ConnectionState.done) return LinearProgressIndicator();
+ if (snapshot.hasError || !snapshot.hasData) return NetworkContentError(refreshCallback: _retry);
+
+ return RefreshIndicator(
+ onRefresh: () async {
+ setState(() {});
+ },
+ child: ListView(
+ children: _buildRecipeCardsFromString(snapshot.data.body),
+ ),
+ );
+ },
+ ),
+ );
+ }
+
+ Future<Response?> _getWeeklyRecipes() async {
+ Map<String, String> _headers = {"Content-Type": "application/json; charset=UTF-8"};
+
+ try {
+ return await Future.delayed(Duration(milliseconds: 500), () {
+ print(SettingsData.settings["serverURL"]);
+ return get(Uri.https(SettingsData.settings["serverURL"]!, "/weekly"), headers: _headers);
+ });
+ } catch (e) {
+ print(e);
+ return null;
+ }
+ }
+
+ List<Widget> _buildRecipeCardsFromString(String string) {
+ List<Widget> _list = [];
+ RecipeData.remoteRecipeList.clear();
+
+ for (dynamic _entry in jsonDecode(string)) {
+ Recipe _recipe = Recipe.fromJson(jsonEncode(_entry));
+ RecipeData.remoteRecipeList.add(_recipe);
+ _list.add(RecipeCard(remote: true, recipe: _recipe));
+ }
+
+ return _list;
+ }
+
+ Future<Null> _retry() async {
+ await Future.delayed(Duration(milliseconds: 300));
+
+ setState(() {});
+
+ return null;
+ }
+}
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,
+ );
+ }
+ }
+ */
+}