aboutsummaryrefslogtreecommitdiff
path: root/lib/views
diff options
context:
space:
mode:
authordavidpkj <davidpenkow1@gmail.com>2022-07-17 19:25:26 +0200
committerdavidpkj <davidpenkow1@gmail.com>2022-07-17 19:25:26 +0200
commitd282f4bb380ce9c445d6bd3a4c9f001bb6b5f501 (patch)
tree023428b7fa249b66a34d0d83c2f0df0ea572ba75 /lib/views
Initial Commit
Diffstat (limited to 'lib/views')
-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
10 files changed, 1290 insertions, 0 deletions
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;
+ }
+}