From 01dbb74a03e64bc13be94660dabf7640cae7c480 Mon Sep 17 00:00:00 2001 From: Tom Bursch Date: Tue, 12 Nov 2024 20:23:05 +0100 Subject: [PATCH 1/5] feat: Tutorial after sign up --- kitchenowl/lib/cubits/auth_cubit.dart | 18 +++++++++++++-- kitchenowl/lib/pages/onboarding_page.dart | 8 ++++--- kitchenowl/lib/pages/signup_page.dart | 2 ++ kitchenowl/lib/pages/tutorial_page.dart | 27 +++++++++++++++++++++++ kitchenowl/lib/router.dart | 10 +++++++++ 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 kitchenowl/lib/pages/tutorial_page.dart diff --git a/kitchenowl/lib/cubits/auth_cubit.dart b/kitchenowl/lib/cubits/auth_cubit.dart index 9f0cf0a1..751f73e1 100644 --- a/kitchenowl/lib/cubits/auth_cubit.dart +++ b/kitchenowl/lib/cubits/auth_cubit.dart @@ -141,10 +141,12 @@ class AuthCubit extends Cubit { } } - void onboard({ + Future onboard({ required String username, required String name, required String password, + Function()? wrongCredentialsCallback, + Function()? correctCredentialsCallback, }) async { emit(const Loading()); if (await ApiService.getInstance().isOnboarding()) { @@ -152,8 +154,15 @@ class AuthCubit extends Cubit { await ApiService.getInstance().onboarding(username, name, password); if (token != null && ApiService.getInstance().isAuthenticated()) { await SecureStorage.getInstance().write(key: 'TOKEN', value: token); + await this.stream.any((s) => s is Authenticated); + if (correctCredentialsCallback != null) { + correctCredentialsCallback(); + } } else { - updateState(); + await updateState(); + if (wrongCredentialsCallback != null) { + wrongCredentialsCallback(); + } } } } @@ -164,6 +173,7 @@ class AuthCubit extends Cubit { required String password, required String email, Function(String?)? wrongCredentialsCallback, + Function()? correctCredentialsCallback, }) async { emit(const Loading()); final (token, msg) = await ApiService.getInstance().signup( @@ -174,6 +184,10 @@ class AuthCubit extends Cubit { ); if (token != null && ApiService.getInstance().isAuthenticated()) { await SecureStorage.getInstance().write(key: 'TOKEN', value: token); + await this.stream.any((s) => s is Authenticated); + if (correctCredentialsCallback != null) { + correctCredentialsCallback(); + } } else { await updateState(); if (ApiService.getInstance().connectionStatus == Connection.connected && diff --git a/kitchenowl/lib/pages/onboarding_page.dart b/kitchenowl/lib/pages/onboarding_page.dart index f74ce37e..bce0862a 100644 --- a/kitchenowl/lib/pages/onboarding_page.dart +++ b/kitchenowl/lib/pages/onboarding_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:kitchenowl/cubits/auth_cubit.dart'; import 'package:kitchenowl/kitchenowl.dart'; import 'package:kitchenowl/widgets/create_user_form_fields.dart'; @@ -44,7 +45,7 @@ class _OnboardingPageState extends State { ), Padding( padding: const EdgeInsets.only(top: 16, bottom: 8), - child: ElevatedButton( + child: LoadingElevatedButton( onPressed: () => _submit(context), child: Text(AppLocalizations.of(context)!.start), ), @@ -67,12 +68,13 @@ class _OnboardingPageState extends State { ); } - void _submit(BuildContext context) { + Future _submit(BuildContext context) async { if (_formKey.currentState!.validate()) { - BlocProvider.of(context).onboard( + await BlocProvider.of(context).onboard( username: usernameController.text, name: nameController.text, password: passwordController.text, + correctCredentialsCallback: () => context.push("/tutorial"), ); } } diff --git a/kitchenowl/lib/pages/signup_page.dart b/kitchenowl/lib/pages/signup_page.dart index 1c39f5e2..6fa231c5 100644 --- a/kitchenowl/lib/pages/signup_page.dart +++ b/kitchenowl/lib/pages/signup_page.dart @@ -198,6 +198,8 @@ class _SignupPageState extends State { Future.delayed( Duration(milliseconds: 1), () => router.go("/register")); }, + correctCredentialsCallback: () => Future.delayed( + Duration(milliseconds: 1), () => router.push("/tutorial")), ); } } diff --git a/kitchenowl/lib/pages/tutorial_page.dart b/kitchenowl/lib/pages/tutorial_page.dart new file mode 100644 index 00000000..0abd5c2c --- /dev/null +++ b/kitchenowl/lib/pages/tutorial_page.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:kitchenowl/kitchenowl.dart'; + +class TutorialPage extends StatelessWidget { + const TutorialPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints.expand(width: 600), + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + "Tutorial", + style: Theme.of(context).textTheme.headlineLarge, + ), + ), + ), + ), + ), + ); + } +} diff --git a/kitchenowl/lib/router.dart b/kitchenowl/lib/router.dart index c9da5eb8..d6df6333 100644 --- a/kitchenowl/lib/router.dart +++ b/kitchenowl/lib/router.dart @@ -28,6 +28,7 @@ import 'package:kitchenowl/pages/settings_user_page.dart'; import 'package:kitchenowl/pages/setup_page.dart'; import 'package:kitchenowl/pages/signup_page.dart'; import 'package:kitchenowl/pages/splash_page.dart'; +import 'package:kitchenowl/pages/tutorial_page.dart'; import 'package:kitchenowl/pages/unreachable_page.dart'; import 'package:kitchenowl/pages/household_page.dart'; import 'package:kitchenowl/pages/unsupported_page.dart'; @@ -184,6 +185,15 @@ final router = GoRouter( return (authState is! Unreachable) ? "/" : null; }, ), + GoRoute( + path: '/tutorial', + pageBuilder: (context, state) => SharedAxisTransitionPage( + key: state.pageKey, + name: state.name, + transitionType: SharedAxisTransitionType.scaled, + child: const TutorialPage(), + ), + ), GoRoute( path: "/household", pageBuilder: (context, state) => SharedAxisTransitionPage( From afcfc8c79a8b9ce7e992ac07b128cd258a1e7e32 Mon Sep 17 00:00:00 2001 From: Tom Bursch Date: Tue, 12 Nov 2024 20:24:42 +0100 Subject: [PATCH 2/5] Fix onboard redirect --- kitchenowl/lib/pages/onboarding_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kitchenowl/lib/pages/onboarding_page.dart b/kitchenowl/lib/pages/onboarding_page.dart index bce0862a..eb935904 100644 --- a/kitchenowl/lib/pages/onboarding_page.dart +++ b/kitchenowl/lib/pages/onboarding_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; import 'package:kitchenowl/cubits/auth_cubit.dart'; import 'package:kitchenowl/kitchenowl.dart'; +import 'package:kitchenowl/router.dart'; import 'package:kitchenowl/widgets/create_user_form_fields.dart'; class OnboardingPage extends StatefulWidget { @@ -74,7 +74,7 @@ class _OnboardingPageState extends State { username: usernameController.text, name: nameController.text, password: passwordController.text, - correctCredentialsCallback: () => context.push("/tutorial"), + correctCredentialsCallback: () => router.push("/tutorial"), ); } } From 885d234c025969729792457882b21112c6f6c9d5 Mon Sep 17 00:00:00 2001 From: Tom Bursch Date: Thu, 5 Dec 2024 13:48:45 +0100 Subject: [PATCH 3/5] Update --- kitchenowl/lib/l10n/app_en.arb | 14 ++ kitchenowl/lib/pages/expense_page.dart | 28 +-- kitchenowl/lib/pages/recipe_page.dart | 46 +--- kitchenowl/lib/pages/signup_page.dart | 12 +- kitchenowl/lib/pages/tutorial_page.dart | 226 +++++++++++++++++- kitchenowl/lib/widgets/_export.dart | 1 + .../lib/widgets/kitchenowl_markdown_body.dart | 51 ++++ .../lib/widgets/recipe_markdown_body.dart | 39 +++ 8 files changed, 333 insertions(+), 84 deletions(-) create mode 100644 kitchenowl/lib/widgets/kitchenowl_markdown_body.dart create mode 100644 kitchenowl/lib/widgets/recipe_markdown_body.dart diff --git a/kitchenowl/lib/l10n/app_en.arb b/kitchenowl/lib/l10n/app_en.arb index 8d6cd25b..4d8ae7f1 100644 --- a/kitchenowl/lib/l10n/app_en.arb +++ b/kitchenowl/lib/l10n/app_en.arb @@ -120,6 +120,11 @@ "@go": {}, "@grid": {}, "@helpTranslate": {}, + "@hi": { + "placeholders": { + "name": {} + } + }, "@household": {}, "@householdDelete": {}, "@householdDeleteConfirmation": { @@ -316,6 +321,7 @@ } }, "@signup": {}, + "@skip": {}, "@smaller": {}, "@sortingAlgorithmic": {}, "@sortingAlphabetical": {}, @@ -339,6 +345,9 @@ "@themeSystem": {}, "@total": {}, "@totalTime": {}, + "@tutorialItemDescription1": {}, + "@tutorialItemDescription2": {}, + "@tutorialRecipeDescription": {}, "@uncategorized": {}, "@underConstruction": {}, "@undo": {}, @@ -459,6 +468,7 @@ "go": "Go", "grid": "Grid", "helpTranslate": "Help translate", + "hi": "Hi {name}!", "household": "Household", "householdDelete": "Delete household", "householdDeleteConfirmation": "Are you sure you want to delete {household}? This will delete any items, recipes, and expenses associated with that household.", @@ -593,6 +603,7 @@ "shoppingLists": "Shopping lists", "signInWith": "Sign in with {provider}", "signup": "Sign up", + "skip": "Skip", "smaller": "Smaller", "sortingAlgorithmic": "Algorithmic", "sortingAlphabetical": "Alphabetical", @@ -612,6 +623,9 @@ "themeSystem": "System", "total": "Total", "totalTime": "Total time", + "tutorialItemDescription1": "", + "tutorialItemDescription2": "", + "tutorialRecipeDescription": "", "uncategorized": "Uncategorized", "underConstruction": "Under construction", "undo": "Undo", diff --git a/kitchenowl/lib/pages/expense_page.dart b/kitchenowl/lib/pages/expense_page.dart index 9ca97231..5c57a140 100644 --- a/kitchenowl/lib/pages/expense_page.dart +++ b/kitchenowl/lib/pages/expense_page.dart @@ -1,13 +1,10 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:intl/intl.dart'; import 'package:kitchenowl/app.dart'; import 'package:kitchenowl/cubits/expense_cubit.dart'; import 'package:kitchenowl/enums/update_enum.dart'; -import 'package:kitchenowl/helpers/url_launcher.dart'; import 'package:kitchenowl/models/expense.dart'; import 'package:kitchenowl/models/household.dart'; import 'package:kitchenowl/pages/expense_add_update_page.dart'; @@ -113,31 +110,8 @@ class _ExpensePageState extends State { margin: const EdgeInsets.fromLTRB(16, 24, 16, 4), child: Padding( padding: const EdgeInsets.all(16), - child: MarkdownBody( + child: KitchenOwlMarkdownBody( data: state.expense.description!, - shrinkWrap: true, - styleSheet: MarkdownStyleSheet.fromTheme( - Theme.of(context), - ).copyWith( - blockquoteDecoration: BoxDecoration( - color: Theme.of(context).cardTheme.color ?? - Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(2.0), - ), - ), - imageBuilder: (uri, title, alt) => - CachedNetworkImage( - imageUrl: uri.toString(), - placeholder: (context, url) => - const CircularProgressIndicator(), - errorWidget: (context, url, error) => - const Icon(Icons.error), - ), - onTapLink: (text, href, title) { - if (href != null && isValidUrl(href)) { - openUrl(context, href); - } - }, ), ), ), diff --git a/kitchenowl/lib/pages/recipe_page.dart b/kitchenowl/lib/pages/recipe_page.dart index b721240d..19e9c9f8 100644 --- a/kitchenowl/lib/pages/recipe_page.dart +++ b/kitchenowl/lib/pages/recipe_page.dart @@ -1,24 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:kitchenowl/app.dart'; import 'package:kitchenowl/cubits/recipe_cubit.dart'; import 'package:kitchenowl/enums/update_enum.dart'; -import 'package:kitchenowl/helpers/recipe_item_markdown_extension.dart'; import 'package:kitchenowl/helpers/share.dart'; -import 'package:kitchenowl/helpers/url_launcher.dart'; import 'package:kitchenowl/models/household.dart'; import 'package:kitchenowl/models/item.dart'; import 'package:kitchenowl/models/recipe.dart'; import 'package:kitchenowl/models/shoppinglist.dart'; import 'package:kitchenowl/pages/recipe_add_update_page.dart'; import 'package:kitchenowl/kitchenowl.dart'; +import 'package:kitchenowl/widgets/recipe_markdown_body.dart'; import 'package:kitchenowl/widgets/recipe_source_chip.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:kitchenowl/widgets/sliver_with_pinned_footer.dart'; -import 'package:markdown/markdown.dart' as md; import 'package:responsive_builder/responsive_builder.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:tuple/tuple.dart'; @@ -129,44 +125,8 @@ class _RecipePageState extends State { ), if (state.recipe.prepTime + state.recipe.cookTime > 0) const SizedBox(height: 16), - MarkdownBody( - data: state.recipe.description, - shrinkWrap: true, - styleSheet: MarkdownStyleSheet.fromTheme( - Theme.of(context), - ).copyWith( - blockquoteDecoration: BoxDecoration( - color: Theme.of(context).cardTheme.color ?? - Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(2.0), - ), - ), - imageBuilder: (uri, title, alt) => CachedNetworkImage( - imageUrl: uri.toString(), - placeholder: (context, url) => - const CircularProgressIndicator(), - errorWidget: (context, url, error) => - const Icon(Icons.error), - ), - onTapLink: (text, href, title) { - if (href != null && isValidUrl(href)) { - openUrl(context, href); - } - }, - builders: { - 'recipeItem': RecipeItemMarkdownBuilder( - cubit: cubit, - ), - }, - extensionSet: md.ExtensionSet( - md.ExtensionSet.gitHubWeb.blockSyntaxes, - md.ExtensionSet.gitHubWeb.inlineSyntaxes + - [ - RecipeItemMarkdownSyntax( - state.recipe, - ), - ], - ), + RecipeMarkdownBody( + recipe: state.recipe, ), ], ), diff --git a/kitchenowl/lib/pages/signup_page.dart b/kitchenowl/lib/pages/signup_page.dart index 6fa231c5..15b9efe3 100644 --- a/kitchenowl/lib/pages/signup_page.dart +++ b/kitchenowl/lib/pages/signup_page.dart @@ -124,15 +124,12 @@ class _SignupPageState extends State { isValidUrl(privacyPolicyUrl)) Padding( padding: const EdgeInsets.only(top: 16), - child: MarkdownBody( + child: KitchenOwlMarkdownBody( data: AppLocalizations.of(context)! .privacyPolicyAgree( "[${AppLocalizations.of(context)!.privacyPolicy}]($privacyPolicyUrl)", ), - shrinkWrap: true, - styleSheet: MarkdownStyleSheet.fromTheme( - Theme.of(context), - ).copyWith( + styleSheet: MarkdownStyleSheet( p: Theme.of(context) .textTheme .labelSmall @@ -148,11 +145,6 @@ class _SignupPageState extends State { Theme.of(context).colorScheme.primary, ), ), - onTapLink: (text, href, title) { - if (href != null && isValidUrl(href)) { - openUrl(context, href); - } - }, ), ), Padding( diff --git a/kitchenowl/lib/pages/tutorial_page.dart b/kitchenowl/lib/pages/tutorial_page.dart index 0abd5c2c..b72d4f20 100644 --- a/kitchenowl/lib/pages/tutorial_page.dart +++ b/kitchenowl/lib/pages/tutorial_page.dart @@ -1,9 +1,30 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:kitchenowl/cubits/auth_cubit.dart'; +import 'package:kitchenowl/item_icons.dart'; import 'package:kitchenowl/kitchenowl.dart'; +import 'package:kitchenowl/models/item.dart'; +import 'package:kitchenowl/models/recipe.dart'; +import 'package:kitchenowl/widgets/recipe_markdown_body.dart'; +import 'package:kitchenowl/widgets/shopping_item.dart'; +import 'package:markdown/markdown.dart' as md; -class TutorialPage extends StatelessWidget { +class TutorialPage extends StatefulWidget { const TutorialPage({super.key}); + @override + State createState() => _TutorialPageState(); +} + +class _TutorialPageState extends State { + int step = 0; + + static const int lastStep = 2; + + static const String _tutorialMarkdown = + "## Instructions\n1. First use @eggs\n2. Now stir"; + @override Widget build(BuildContext context) { return Scaffold( @@ -14,9 +35,178 @@ class TutorialPage extends StatelessWidget { constraints: const BoxConstraints.expand(width: 600), child: Padding( padding: const EdgeInsets.all(16), - child: Text( - "Tutorial", - style: Theme.of(context).textTheme.headlineLarge, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), + child: TweenAnimationBuilder( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + tween: Tween( + begin: 1 / (lastStep + 1), + end: (step + 1) / (lastStep + 1), + ), + builder: (context, value, _) => LinearProgressIndicator( + value: value, + borderRadius: BorderRadius.circular(30), + ), + ), + ), + Text( + AppLocalizations.of(context)!.hi( + BlocProvider.of(context).getUser()?.name ?? + ""), + style: Theme.of(context).textTheme.displayMedium, + textAlign: TextAlign.start, + ), + Expanded( + child: AbsorbPointer( + absorbing: true, + child: IndexedStack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + Expanded( + child: Text(AppLocalizations.of(context)! + .tutorialItemDescription1), + ), + SearchTextField( + controller: TextEditingController.fromValue( + TextEditingValue(text: "300g Tomatoes")), + onSearch: (s) => Future.value(), + ), + const SizedBox(height: 8), + LayoutBuilder( + builder: (context, constraints) => SizedBox( + width: constraints.maxWidth / 3, + child: AspectRatio( + aspectRatio: 1, + child: ShoppingItemWidget( + item: ItemWithDescription( + name: "Tomatoes", + description: "300g", + icon: "tomato", + ), + gridStyle: true, + selected: true, + ), + ), + ), + ), + const Spacer(flex: 2), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + Expanded( + child: Text(AppLocalizations.of(context)! + .tutorialItemDescription2), + ), + SearchTextField( + controller: TextEditingController.fromValue( + TextEditingValue(text: "Cake, Frozen")), + onSearch: (s) => Future.value(), + ), + const SizedBox(height: 8), + LayoutBuilder( + builder: (context, constraints) => SizedBox( + width: constraints.maxWidth / 3, + child: AspectRatio( + aspectRatio: 1, + child: ShoppingItemWidget( + item: ItemWithDescription( + name: "Cake", + description: "Frozen", + icon: "cake", + ), + gridStyle: true, + selected: true, + ), + ), + ), + ), + const Spacer(flex: 2), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + Expanded( + child: Text(AppLocalizations.of(context)! + .tutorialRecipeDescription), + ), + Text(_tutorialMarkdown), + Divider(), + RecipeMarkdownBody( + recipeItemBuilder: + _TutorialRecipeItemMarkdownBuilder( + RecipeItem( + name: "Eggs", + description: "2", + icon: "eggs", + ), + ), + recipe: Recipe( + description: _tutorialMarkdown, + items: [ + RecipeItem( + name: "Eggs", + description: "2", + icon: "eggs", + ), + ], + ), + ), + const Spacer(flex: 2), + ], + ) + ], + index: step, + ), + ), + ), + Row( + children: [ + TextButton( + onPressed: () { + if (step == 0) + Navigator.of(context).pop(); + else + setState(() { + step--; + }); + }, + child: Text( + step == 0 + ? AppLocalizations.of(context)!.skip + : AppLocalizations.of(context)!.back, + ), + ), + const Spacer(), + ElevatedButton( + onPressed: () { + if (step < lastStep) + setState(() { + step++; + }); + else + Navigator.of(context).pop(); + }, + child: Text( + step < lastStep + ? AppLocalizations.of(context)!.next + : AppLocalizations.of(context)!.okay, + ), + ), + ], + ), + ], ), ), ), @@ -25,3 +215,31 @@ class TutorialPage extends StatelessWidget { ); } } + +class _TutorialRecipeItemMarkdownBuilder extends MarkdownElementBuilder { + final RecipeItem item; + _TutorialRecipeItemMarkdownBuilder(this.item); + + @override + Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) { + IconData? icon = ItemIcons.get(item); + return RichText( + text: TextSpan(children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Chip( + avatar: icon != null ? Icon(icon) : null, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: EdgeInsets.zero, + labelPadding: icon != null + ? const EdgeInsets.only(left: 1, right: 4) + : null, + label: Text(item.name + + (item.description.isNotEmpty + ? " (${item.description})" + : "")), + )), + ]), + ); + } +} diff --git a/kitchenowl/lib/widgets/_export.dart b/kitchenowl/lib/widgets/_export.dart index c483aabe..bd4ce770 100644 --- a/kitchenowl/lib/widgets/_export.dart +++ b/kitchenowl/lib/widgets/_export.dart @@ -30,3 +30,4 @@ export 'loading_list_tile.dart'; export 'loading_elevated_button_icon.dart'; export 'loading_filled_button.dart'; export 'kitchenowl_search_anchor.dart'; +export 'kitchenowl_markdown_body.dart'; diff --git a/kitchenowl/lib/widgets/kitchenowl_markdown_body.dart b/kitchenowl/lib/widgets/kitchenowl_markdown_body.dart new file mode 100644 index 00000000..f7022a06 --- /dev/null +++ b/kitchenowl/lib/widgets/kitchenowl_markdown_body.dart @@ -0,0 +1,51 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:kitchenowl/helpers/url_launcher.dart'; + +class KitchenOwlMarkdownBody extends StatelessWidget { + final String data; + final Map builders; + final MarkdownStyleSheet? styleSheet; + final md.ExtensionSet? extensionSet; + + const KitchenOwlMarkdownBody({ + super.key, + required this.data, + this.builders = const {}, + this.styleSheet, + this.extensionSet, + }); + + @override + Widget build(BuildContext context) { + return MarkdownBody( + data: data, + shrinkWrap: true, + styleSheet: MarkdownStyleSheet.fromTheme( + Theme.of(context), + ) + .copyWith( + blockquoteDecoration: BoxDecoration( + color: Theme.of(context).cardTheme.color ?? + Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(2.0), + ), + ) + .merge(styleSheet), + imageBuilder: (uri, title, alt) => CachedNetworkImage( + imageUrl: uri.toString(), + placeholder: (context, url) => const CircularProgressIndicator(), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + onTapLink: (text, href, title) { + if (href != null && isValidUrl(href)) { + openUrl(context, href); + } + }, + builders: builders, + extensionSet: extensionSet, + ); + } +} diff --git a/kitchenowl/lib/widgets/recipe_markdown_body.dart b/kitchenowl/lib/widgets/recipe_markdown_body.dart new file mode 100644 index 00000000..3aa4ec23 --- /dev/null +++ b/kitchenowl/lib/widgets/recipe_markdown_body.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:kitchenowl/cubits/recipe_cubit.dart'; +import 'package:kitchenowl/helpers/recipe_item_markdown_extension.dart'; +import 'package:kitchenowl/kitchenowl.dart'; +import 'package:kitchenowl/models/recipe.dart'; +import 'package:markdown/markdown.dart' as md; + +class RecipeMarkdownBody extends StatelessWidget { + final Recipe recipe; + final MarkdownElementBuilder? recipeItemBuilder; + + const RecipeMarkdownBody({ + super.key, + required this.recipe, + this.recipeItemBuilder, + }); + + @override + Widget build(BuildContext context) { + return KitchenOwlMarkdownBody( + data: recipe.description, + builders: { + 'recipeItem': recipeItemBuilder ?? + RecipeItemMarkdownBuilder( + cubit: BlocProvider.of(context), + ), + }, + extensionSet: md.ExtensionSet( + md.ExtensionSet.gitHubWeb.blockSyntaxes, + md.ExtensionSet.gitHubWeb.inlineSyntaxes + + [ + RecipeItemMarkdownSyntax(recipe), + ], + ), + ); + } +} From cb2175399433450b6131ff6c96afcc4b3e561f84 Mon Sep 17 00:00:00 2001 From: Tom Bursch Date: Fri, 13 Dec 2024 12:08:21 +0100 Subject: [PATCH 4/5] Update --- kitchenowl/lib/l10n/app_en.arb | 6 +-- kitchenowl/lib/pages/tutorial_page.dart | 56 +++++++++++++++---------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/kitchenowl/lib/l10n/app_en.arb b/kitchenowl/lib/l10n/app_en.arb index 4d8ae7f1..2a0fb321 100644 --- a/kitchenowl/lib/l10n/app_en.arb +++ b/kitchenowl/lib/l10n/app_en.arb @@ -623,9 +623,9 @@ "themeSystem": "System", "total": "Total", "totalTime": "Total time", - "tutorialItemDescription1": "", - "tutorialItemDescription2": "", - "tutorialRecipeDescription": "", + "tutorialItemDescription1": "When searching for items you can add an amount at the start of your query and it will be added as a description.", + "tutorialItemDescription2": "Or, if amounts are not enough, use a comma to seperate the item name and description.", + "tutorialRecipeDescription": "Inside of recipes you can use Markdown. The most commong things are headlines specified with hash signs, numbered lists for instruction steps, and references to ingredients using an at sign.", "uncategorized": "Uncategorized", "underConstruction": "Under construction", "undo": "Undo", diff --git a/kitchenowl/lib/pages/tutorial_page.dart b/kitchenowl/lib/pages/tutorial_page.dart index b72d4f20..c95cae10 100644 --- a/kitchenowl/lib/pages/tutorial_page.dart +++ b/kitchenowl/lib/pages/tutorial_page.dart @@ -72,6 +72,7 @@ class _TutorialPageState extends State { Expanded( child: Text(AppLocalizations.of(context)! .tutorialItemDescription1), + flex: 2, ), SearchTextField( controller: TextEditingController.fromValue( @@ -96,7 +97,7 @@ class _TutorialPageState extends State { ), ), ), - const Spacer(flex: 2), + const Spacer(flex: 3), ], ), Column( @@ -106,6 +107,7 @@ class _TutorialPageState extends State { Expanded( child: Text(AppLocalizations.of(context)! .tutorialItemDescription2), + flex: 2, ), SearchTextField( controller: TextEditingController.fromValue( @@ -130,7 +132,7 @@ class _TutorialPageState extends State { ), ), ), - const Spacer(flex: 2), + const Spacer(flex: 3), ], ), Column( @@ -140,28 +142,40 @@ class _TutorialPageState extends State { Expanded( child: Text(AppLocalizations.of(context)! .tutorialRecipeDescription), + flex: 2, ), - Text(_tutorialMarkdown), - Divider(), - RecipeMarkdownBody( - recipeItemBuilder: - _TutorialRecipeItemMarkdownBuilder( - RecipeItem( - name: "Eggs", - description: "2", - icon: "eggs", + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: Text(_tutorialMarkdown)), + SizedBox( + height: 120, + width: 60, + child: VerticalDivider(), ), - ), - recipe: Recipe( - description: _tutorialMarkdown, - items: [ - RecipeItem( - name: "Eggs", - description: "2", - icon: "eggs", + Expanded( + child: RecipeMarkdownBody( + recipeItemBuilder: + _TutorialRecipeItemMarkdownBuilder( + RecipeItem( + name: "Eggs", + description: "2", + icon: "eggs", + ), + ), + recipe: Recipe( + description: _tutorialMarkdown, + items: [ + RecipeItem( + name: "Eggs", + description: "2", + icon: "eggs", + ), + ], + ), ), - ], - ), + ), + ], ), const Spacer(flex: 2), ], From 8b8b5ead1f23a7c5af71d58c907148c722b5883b Mon Sep 17 00:00:00 2001 From: Tom Bursch Date: Mon, 16 Dec 2024 19:29:30 +0100 Subject: [PATCH 5/5] Update --- kitchenowl/lib/l10n/app_en.arb | 8 ++- kitchenowl/lib/pages/tutorial_page.dart | 66 +++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/kitchenowl/lib/l10n/app_en.arb b/kitchenowl/lib/l10n/app_en.arb index 2a0fb321..3193784e 100644 --- a/kitchenowl/lib/l10n/app_en.arb +++ b/kitchenowl/lib/l10n/app_en.arb @@ -348,6 +348,11 @@ "@tutorialItemDescription1": {}, "@tutorialItemDescription2": {}, "@tutorialRecipeDescription": {}, + "@tutorialRecipeMore": { + "placeholders": { + "url": {} + } + }, "@uncategorized": {}, "@underConstruction": {}, "@undo": {}, @@ -625,7 +630,8 @@ "totalTime": "Total time", "tutorialItemDescription1": "When searching for items you can add an amount at the start of your query and it will be added as a description.", "tutorialItemDescription2": "Or, if amounts are not enough, use a comma to seperate the item name and description.", - "tutorialRecipeDescription": "Inside of recipes you can use Markdown. The most commong things are headlines specified with hash signs, numbered lists for instruction steps, and references to ingredients using an at sign.", + "tutorialRecipeDescription": "Inside of recipes you can use Markdown. Basic features are headlines specified with hash signs, numbered lists for instruction steps, and references to ingredients using an at sign and the ingredient name.", + "tutorialRecipeMore": "For more information visit the [documentation]({url}).", "uncategorized": "Uncategorized", "underConstruction": "Under construction", "undo": "Undo", diff --git a/kitchenowl/lib/pages/tutorial_page.dart b/kitchenowl/lib/pages/tutorial_page.dart index c95cae10..e6482af0 100644 --- a/kitchenowl/lib/pages/tutorial_page.dart +++ b/kitchenowl/lib/pages/tutorial_page.dart @@ -62,7 +62,7 @@ class _TutorialPageState extends State { ), Expanded( child: AbsorbPointer( - absorbing: true, + absorbing: step != 2, child: IndexedStack( children: [ Column( @@ -111,7 +111,8 @@ class _TutorialPageState extends State { ), SearchTextField( controller: TextEditingController.fromValue( - TextEditingValue(text: "Cake, Frozen")), + TextEditingValue( + text: "Raspberry, Frozen")), onSearch: (s) => Future.value(), ), const SizedBox(height: 8), @@ -122,9 +123,9 @@ class _TutorialPageState extends State { aspectRatio: 1, child: ShoppingItemWidget( item: ItemWithDescription( - name: "Cake", + name: "Raspberry", description: "Frozen", - icon: "cake", + icon: "raspberry", ), gridStyle: true, selected: true, @@ -144,6 +145,38 @@ class _TutorialPageState extends State { .tutorialRecipeDescription), flex: 2, ), + Row( + children: [ + Expanded( + child: Text( + "Markdown", + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith( + color: Theme.of(context) + .textTheme + .labelMedium + ?.color + ?.withAlpha(85), + ), + )), + Text( + AppLocalizations.of(context)!.recipes, + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith( + color: Theme.of(context) + .textTheme + .labelMedium + ?.color + ?.withAlpha(85), + ), + ), + ], + ), + const SizedBox(height: 15), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -177,7 +210,30 @@ class _TutorialPageState extends State { ), ], ), - const Spacer(flex: 2), + const Spacer(flex: 1), + KitchenOwlMarkdownBody( + data: AppLocalizations.of(context)! + .tutorialRecipeMore( + "https://docs.kitchenowl.org/latest/Tips-%26-Tricks/markdown/", + ), + styleSheet: MarkdownStyleSheet( + p: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith( + color: Theme.of(context) + .textTheme + .labelMedium + ?.color + ?.withAlpha(76), + ), + a: TextStyle( + color: + Theme.of(context).colorScheme.primary, + ), + ), + ), + const Spacer(flex: 1), ], ) ],