diff --git a/lib/components/appBar.dart b/lib/components/appBar.dart index ce8e16c..1fafb18 100644 --- a/lib/components/appBar.dart +++ b/lib/components/appBar.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; class FAppBar extends StatelessWidget implements PreferredSizeWidget { - const FAppBar({super.key}); + final List actions; + + const FAppBar({super.key, required this.actions}); @override Widget build(BuildContext context) { @@ -13,6 +15,7 @@ class FAppBar extends StatelessWidget implements PreferredSizeWidget { child: AppBar( backgroundColor: Colors.transparent, elevation: 0, + actions: actions, ) ); } diff --git a/lib/components/button.dart b/lib/components/button.dart index feb43e1..93ce07a 100644 --- a/lib/components/button.dart +++ b/lib/components/button.dart @@ -21,8 +21,20 @@ class FButton extends StatelessWidget { child: Container( padding: EdgeInsets.symmetric(vertical: insidePadding), decoration: BoxDecoration( - color: colorScheme.primary, + gradient: LinearGradient( + colors: [ + colorScheme.primary.withOpacity(0.5), + colorScheme.secondary.withOpacity(0.5), + ], + ), borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.3), + blurRadius: 5, + offset: const Offset(0, 5), + ) + ], ), child: Center( child: Text( diff --git a/lib/components/datePicker.dart b/lib/components/datePicker.dart index 1033312..4992c46 100644 --- a/lib/components/datePicker.dart +++ b/lib/components/datePicker.dart @@ -28,11 +28,12 @@ class FDateItemWidget extends StatelessWidget { onDatePicked(date); }, child: Container( - width: 100, + width: picked? 100: 50, decoration: BoxDecoration( borderRadius: BorderRadius.circular(50), border: Border.all( - color: colorScheme.onPrimary, + // color: picked ? colorScheme.onPrimary : Colors.transparent, + color: Colors.transparent, width: 2, ), color: picked ? colorScheme.onPrimary.withOpacity(0.25) : Colors.transparent, @@ -44,7 +45,7 @@ class FDateItemWidget extends StatelessWidget { dayOfTheWeekMap[date.weekday]!, style: TextStyle( color: colorScheme.onPrimary, - fontSize: 24, + fontSize: picked ? 24: 12, fontWeight: FontWeight.bold, ), ), @@ -52,7 +53,7 @@ class FDateItemWidget extends StatelessWidget { '${date.day}.${date.month}', style: TextStyle( color: colorScheme.onPrimary, - fontSize: 24, + fontSize: picked ? 24: 12, fontWeight: FontWeight.bold, ), ), @@ -102,28 +103,35 @@ class _FDatePickerWidgetState extends State { child: Center( child: ListView( scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 20), shrinkWrap: true, children: [ - SizedBox(width: 25), + FDateItemWidget(date: date.add(Duration(days: -3)), onDatePicked: onDatePicked), FDateItemWidget(date: date.add(Duration(days: -2)), onDatePicked: onDatePicked), - SizedBox(width: 25), FDateItemWidget(date: date.add(Duration(days: -1)), onDatePicked: onDatePicked), - SizedBox(width: 25), FDateItemWidget(date: date, onDatePicked: onDatePicked, picked: true), - SizedBox(width: 25), FDateItemWidget(date: date.add(Duration(days: 1)), onDatePicked: onDatePicked), - SizedBox(width: 25), FDateItemWidget(date: date.add(Duration(days: 2)), onDatePicked: onDatePicked), Container( - width: 100, + width: 50, child: IconButton( icon: Icon( Icons.calendar_month, color: colorScheme.onPrimary, - size: 40, + size: 20, ), onPressed: () { - onDatePicked(date.add(Duration(days: 1))); + // open date picker + showDatePicker( + context: context, + initialDate: date, + firstDate: date.add(Duration(days: -365)), + lastDate: date.add(Duration(days: 365)), + ).then((value) { + if (value != null) { + onDatePicked(value); + } + }); }, ), ), diff --git a/lib/components/navigationBar.dart b/lib/components/navigationBar.dart new file mode 100644 index 0000000..f31ab6b --- /dev/null +++ b/lib/components/navigationBar.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:blur/blur.dart'; + +class FNavBar extends StatelessWidget { + final List children; + final double height; + + const FNavBar ({super.key, required this.children, this.height = 56}); + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + + return SafeArea( + child: Padding( + padding: EdgeInsets.all(12), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.3), + blurRadius: 5, + offset: const Offset(0, 5), + ) + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Stack( + children: [ + Blur( + blur: 10, + blurColor: colorScheme.primary.withOpacity(0.1), + child: Container( + width: MediaQuery.of(context).size.width, + height: height * children.length, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + colorScheme.primary.withOpacity(0.1), + colorScheme.secondary.withOpacity(0.1), + ], + ), + ), + ), + ), + Container( + width: MediaQuery.of(context).size.width, + height: height * children.length, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ), + ), + ], + ) + ), + ), + ), + ); + } +} diff --git a/lib/components/sliver.dart b/lib/components/sliver.dart index ac1e21d..5c52625 100644 --- a/lib/components/sliver.dart +++ b/lib/components/sliver.dart @@ -1,4 +1,48 @@ import 'package:flutter/material.dart'; +import 'package:blur/blur.dart'; + + +class ClipShadowPath extends StatelessWidget { + final Shadow shadow; + final CustomClipper clipper; + final Widget child; + + ClipShadowPath({ + required this.shadow, + required this.clipper, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: _ClipShadowShadowPainter( + clipper: this.clipper, + shadow: this.shadow, + ), + child: ClipPath(child: child, clipper: this.clipper), + ); + } +} + +class _ClipShadowShadowPainter extends CustomPainter { + final Shadow shadow; + final CustomClipper clipper; + + _ClipShadowShadowPainter({required this.shadow, required this.clipper}); + + @override + void paint(Canvas canvas, Size size) { + var paint = shadow.toPaint(); + var clipPath = clipper.getClip(size).shift(shadow.offset); + canvas.drawPath(clipPath, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} class BackgroundWave extends StatelessWidget { final double height; @@ -12,16 +56,30 @@ class BackgroundWave extends StatelessWidget { return SizedBox( height: height, - child: ClipPath( - clipper: BackgroundWaveClipper(), + child: ClipShadowPath( + clipper: BackgroundWaveClipper(), + shadow: BoxShadow( + blurRadius: 5, + color: colorScheme.primary.withOpacity(0.3), + offset: const Offset(0, 5), + ), + child: Blur( + blur: 10, + blurColor: colorScheme.primary.withOpacity(0.1), child: Container( width: MediaQuery.of(context).size.width, height: height, decoration: BoxDecoration( - gradient: LinearGradient( - colors: [colorScheme.primary, colorScheme.secondary], - )), - )), + gradient: LinearGradient( + colors: [ + colorScheme.primary.withOpacity(0.1), + colorScheme.secondary.withOpacity(0.1), + ], + ), + ), + ), + ), + ), ); } } @@ -33,14 +91,14 @@ class BackgroundWaveClipper extends CustomClipper { path.lineTo(0.0, size.height); var firstCurve = Offset(0, size.height - 20); - var lastCurve = Offset(30, size.height - 20); + var lastCurve = Offset(40, size.height - 20); path.quadraticBezierTo( firstCurve.dx, firstCurve.dy, lastCurve.dx, lastCurve.dy, ); firstCurve = Offset(0, size.height - 20); - lastCurve = Offset(size.width - 30, size.height - 20); + lastCurve = Offset(size.width - 40, size.height - 20); path.quadraticBezierTo( firstCurve.dx, firstCurve.dy, lastCurve.dx, lastCurve.dy, @@ -72,13 +130,17 @@ class FSliverAppBar extends SliverPersistentHeaderDelegate { var adjustedShrinkOffset = shrinkOffset > minExtent ? minExtent : shrinkOffset; double offset = (minExtent - adjustedShrinkOffset); + if (offset < 4) { + offset = 4; + } + return Stack( children: [ const BackgroundWave( height: 280, ), Positioned( - top: offset + 8, + top: offset, child: child, left: 16, right: 16, diff --git a/lib/screens/add_entry.dart b/lib/screens/add_entry.dart index c9d94f2..8a46752 100644 --- a/lib/screens/add_entry.dart +++ b/lib/screens/add_entry.dart @@ -20,7 +20,7 @@ class AddEntryScreen extends BasedScreen { State createState() => _AddEntryScreen(); } -class _AddEntryScreen extends State { +class _AddEntryScreen extends BasedState { final gramsController = TextEditingController(); final productNameController = TextEditingController(); Meal? meal; @@ -129,7 +129,7 @@ class _AddEntryScreen extends State { Widget build(BuildContext context) { return Scaffold( extendBodyBehindAppBar: true, - appBar: FAppBar(), + appBar: appBar(), body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 720), diff --git a/lib/screens/based.dart b/lib/screens/based.dart index f725e2a..4aaf874 100644 --- a/lib/screens/based.dart +++ b/lib/screens/based.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; import 'package:fooder/client.dart'; +import 'package:fooder/components/appBar.dart'; +import 'package:fooder/components/navigationBar.dart'; +import 'package:fooder/screens/login.dart'; +import 'package:fooder/screens/main.dart'; TextStyle logoStyle(context) { return Theme.of(context).textTheme.labelLarge!.copyWith( @@ -14,6 +18,79 @@ abstract class BasedScreen extends StatefulWidget { } abstract class BasedState extends State { + void _logout() async { + await widget.apiClient.logout(); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => LoginScreen(apiClient: widget.apiClient), + ), + ); + } + + void backToDiary() { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => MainScreen(apiClient: widget.apiClient), + ), + ); + } + + FAppBar appBar() { + return FAppBar( + actions: [ + IconButton( + icon: Icon( + Icons.logout, + color: Theme.of(context).colorScheme.onPrimary, + ), + onPressed: _logout, + ), + ], + ); + } + + FNavBar navBar() { + return FNavBar( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: Icon( + Icons.menu_book, + color: Theme.of(context).colorScheme.onPrimary, + ), + onPressed: backToDiary, + ), + IconButton( + icon: Icon( + Icons.dinner_dining, + color: Theme.of(context).colorScheme.onPrimary, + ), + onPressed: () => null, + ), + IconButton( + icon: Icon( + Icons.lunch_dining, + color: Theme.of(context).colorScheme.onPrimary, + ), + onPressed: () => null, + ), + IconButton( + icon: Icon( + Icons.person, + color: Theme.of(context).colorScheme.onPrimary, + ), + onPressed: () => null, + ), + ], + ), + ], + ); + } + void showError(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -22,9 +99,10 @@ abstract class BasedState extends State { textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge!.copyWith( fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onError, ), ), - backgroundColor: Theme.of(context).colorScheme.error, + backgroundColor: Theme.of(context).colorScheme.error.withOpacity(0.8), ), ); } @@ -37,9 +115,10 @@ abstract class BasedState extends State { textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge!.copyWith( fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onPrimary, ), ), - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.8), ), ); } diff --git a/lib/screens/login.dart b/lib/screens/login.dart index 2ad8f60..e7958e8 100644 --- a/lib/screens/login.dart +++ b/lib/screens/login.dart @@ -94,7 +94,7 @@ class _LoginScreen extends BasedState { Icon( Icons.lock, size: 100, - color: colorScheme.primary, + color: colorScheme.primary.withOpacity(0.85), ), FTextInput( labelText: 'Username', diff --git a/lib/screens/main.dart b/lib/screens/main.dart index 9c93fea..f169034 100644 --- a/lib/screens/main.dart +++ b/lib/screens/main.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import 'package:fooder/screens/based.dart'; -import 'package:fooder/screens/login.dart'; import 'package:fooder/screens/add_entry.dart'; +import 'package:fooder/screens/add_meal.dart'; import 'package:fooder/models/diary.dart'; import 'package:fooder/widgets/diary.dart'; +import 'package:fooder/widgets/summary.dart'; import 'package:fooder/widgets/meal.dart'; -import 'package:fooder/components/appBar.dart'; +import 'package:fooder/widgets/macroEntry.dart'; import 'package:fooder/components/sliver.dart'; import 'package:fooder/components/datePicker.dart'; +import 'package:blur/blur.dart'; class MainScreen extends BasedScreen { const MainScreen({super.key, required super.apiClient}); @@ -43,16 +45,6 @@ class _MainScreen extends BasedState { await _asyncInitState(); } - void _logout() async { - await widget.apiClient.logout(); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => LoginScreen(apiClient: widget.apiClient), - ), - ); - } - Future _addEntry() async { await Navigator.push( context, @@ -63,10 +55,65 @@ class _MainScreen extends BasedState { ).then((_) => _asyncInitState()); } + Widget floatingActionButton(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + + return Container( + height: 64, + width: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.3), + blurRadius: 5, + offset: const Offset(0, 5), + ) + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(32), + child: Stack( + children: [ + Blur( + blur: 10, + blurColor: colorScheme.primary.withOpacity(0.1), + child: Container( + height: 64, + width: 64, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + colorScheme.primary.withOpacity(0.1), + colorScheme.secondary.withOpacity(0.1), + ], + ), + ), + ), + ), + Container( + height: 64, + width: 64, + child: FloatingActionButton( + elevation: 0, + onPressed: _addEntry, + backgroundColor: Colors.transparent, + child: Icon( + Icons.library_add, + color: colorScheme.onPrimary, + ), + ), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { Widget content; - Widget title; if (diary != null) { content = CustomScrollView( @@ -78,11 +125,17 @@ class _MainScreen extends BasedState { SliverList( delegate: SliverChildListDelegate( [ - for (var meal in diary!.meals) + SummaryWidget( + diary: diary!, + apiClient: widget.apiClient, + refreshParent: _asyncInitState, + ), + for (var (i, meal) in diary!.meals.indexed) MealWidget( meal: meal, apiClient: widget.apiClient, refreshParent: _asyncInitState, + initiallyExpanded: i == 0, ), ], ), @@ -93,14 +146,15 @@ class _MainScreen extends BasedState { content = const Center(child: const CircularProgressIndicator()); } + return Scaffold( body: content, extendBodyBehindAppBar: true, - appBar: FAppBar(), - floatingActionButton: FloatingActionButton( - onPressed: _addEntry, - child: const Icon(Icons.add), - ), + extendBody: true, + appBar: appBar(), + bottomNavigationBar: navBar(), + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + floatingActionButton: floatingActionButton(context), ); } } diff --git a/lib/screens/register.dart b/lib/screens/register.dart index 32d7eb2..9492c8e 100644 --- a/lib/screens/register.dart +++ b/lib/screens/register.dart @@ -77,7 +77,7 @@ class _RegisterScreen extends BasedState { Icon( Icons.group_add, size: 100, - color: colorScheme.primary, + color: colorScheme.primary.withOpacity(0.85), ), FTextInput( labelText: 'Username', diff --git a/lib/widgets/diary.dart b/lib/widgets/diary.dart index a67bed1..ad219ba 100644 --- a/lib/widgets/diary.dart +++ b/lib/widgets/diary.dart @@ -80,11 +80,12 @@ class DiaryWidget extends StatelessWidget { SliverList( delegate: SliverChildListDelegate( [ - for (var meal in diary.meals) + for (var (i, meal) in diary.meals.indexed) MealWidget( meal: meal, apiClient: apiClient, refreshParent: refreshParent, + initiallyExpanded: i == 0, ), ], ), diff --git a/lib/widgets/entry.dart b/lib/widgets/entry.dart index d4a9351..89fed66 100644 --- a/lib/widgets/entry.dart +++ b/lib/widgets/entry.dart @@ -1,8 +1,45 @@ import 'package:flutter/material.dart'; import 'package:fooder/models/entry.dart'; -import 'package:fooder/widgets/macro.dart'; +import 'package:fooder/widgets/macroEntry.dart'; import 'dart:core'; + +class EntryHeader extends StatelessWidget { + final Entry entry; + + const EntryHeader( + {super.key, + required this.entry}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Text( + entry.product.name, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + Spacer(), + Padding( + padding: const EdgeInsets.all(8), + child: Text( + entry.grams.toStringAsFixed(0) + " g", + style: Theme.of(context).textTheme.bodyText2!.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ], + ); + } +} + class EntryWidget extends StatelessWidget { final Entry entry; @@ -14,25 +51,12 @@ class EntryWidget extends StatelessWidget { padding: const EdgeInsets.all(8), child: Column( children: [ - Row( - children: [ - Expanded( - child: Text( - entry.product.name, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - Text("${entry.calories.toStringAsFixed(1)} kcal"), - ], - ), - MacroWidget( + EntryHeader(entry: entry), + MacroEntryWidget( protein: entry.protein, carb: entry.carb, fat: entry.fat, - amount: entry.grams, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.secondary, - ), + calories: entry.calories, ), ], ), diff --git a/lib/widgets/macroEntry.dart b/lib/widgets/macroEntry.dart new file mode 100644 index 0000000..2aeaadd --- /dev/null +++ b/lib/widgets/macroEntry.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'dart:core'; + + +class MacroHeaderWidget extends StatelessWidget { + static final double PAD_Y = 4; + static final double PAD_X = 8; + + final bool? fiber; + final bool? calories; + + const MacroHeaderWidget( + { + super.key, + this.fiber = false, + this.calories = false, + } + ); + + + @override + Widget build(BuildContext context) { + var elements = [ + "C(g)", + "F(g)", + "P(g)", + ]; + + if (fiber == true) { + elements.add( + "F(g)", + ); + } + + if (calories == true) { + elements.add( + "kcal", + ); + } + + var children = []; + + for (var element in elements) { + children.add( + Padding( + padding: EdgeInsets.symmetric( + horizontal: 2, + ), + child: SizedBox( + width: 55, + child: Text( + element, + style: Theme.of(context).textTheme.bodyText1!.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ), + textAlign: TextAlign.center, + ), + ), + ), + ); + } + + children.add(Spacer()); + + return Padding( + padding: EdgeInsets.symmetric( + vertical: PAD_Y, + horizontal: PAD_X, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: children, + ), + ); + } +} + + +class MacroEntryWidget extends StatelessWidget { + static final double PAD_Y = 4; + static final double PAD_X = 8; + + final double protein; + final double carb; + final double fat; + final double? fiber; + final double? calories; + + const MacroEntryWidget( + { + super.key, + required this.protein, + required this.carb, + required this.fat, + this.fiber, + this.calories, + } + ); + + + @override + Widget build(BuildContext context) { + var elements = [ + "${carb.toStringAsFixed(1)}", + "${fat.toStringAsFixed(1)}", + "${protein.toStringAsFixed(1)}", + ]; + + if (fiber != null) { + elements.add( + "${fiber!.toStringAsFixed(1)}", + ); + } + + if (calories != null) { + elements.add( + "${calories!.toStringAsFixed(0)}", + ); + } + + var children = []; + + for (var element in elements) { + children.add( + Padding( + padding: EdgeInsets.symmetric( + horizontal: 2, + ), + child: SizedBox( + width: 55, + child: Text( + element, + style: Theme.of(context).textTheme.bodyText1!.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ), + textAlign: TextAlign.center, + ), + ), + ), + ); + } + + children.add(Spacer()); + + return Padding( + padding: EdgeInsets.symmetric( + vertical: PAD_Y, + horizontal: PAD_X, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: children, + ), + ); + } +} diff --git a/lib/widgets/meal.dart b/lib/widgets/meal.dart index ebf927d..e9156dc 100644 --- a/lib/widgets/meal.dart +++ b/lib/widgets/meal.dart @@ -1,93 +1,152 @@ import 'package:flutter/material.dart'; import 'package:fooder/models/meal.dart'; import 'package:fooder/widgets/entry.dart'; -import 'package:fooder/widgets/macro.dart'; +import 'package:fooder/widgets/macroEntry.dart'; import 'package:fooder/screens/edit_entry.dart'; import 'package:fooder/screens/meal.dart'; import 'package:fooder/client.dart'; import 'dart:core'; -class MealWidget extends StatelessWidget { - final Meal meal; - final ApiClient apiClient; - final Function() refreshParent; - const MealWidget( +class MealHeader extends StatelessWidget { + final Meal meal; + + const MealHeader( {super.key, - required this.meal, - required this.apiClient, - required this.refreshParent}); + required this.meal}); @override Widget build(BuildContext context) { - return Card( - child: GestureDetector( - onLongPress: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MealScreen( - apiClient: apiClient, - meal: meal, - refresh: refreshParent, + return Row( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Text( + meal.name, + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.bold, ), ), - ).then((_) { - refreshParent(); - }); - }, - child: Padding( - padding: const EdgeInsets.only( - top: 36.0, left: 6.0, right: 6.0, bottom: 6.0), - child: ExpansionTile( - title: Column( - children: [ - Row( - children: [ - Expanded( - child: Text( - meal.name, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - Text("${meal.calories.toStringAsFixed(1)} kcal"), - ], - ), - MacroWidget( - protein: meal.protein, - carb: meal.carb, - fat: meal.fat, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.secondary, - ), - ), - ], - ), - children: [ - for (var entry in meal.entries) - ListTile( - title: EntryWidget( - entry: entry, - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EditEntryScreen( - apiClient: apiClient, - entry: entry, - ), - ), - ).then((_) { - refreshParent(); - }); - }, - ) - ], ), - ), - )); + ], + ); + } +} + + +class MealWidget extends StatelessWidget { + static final MAX_WIDTH = 920.0; + + final Meal meal; + final ApiClient apiClient; + final Function() refreshParent; + final bool initiallyExpanded; + + const MealWidget( + { + super.key, + required this.meal, + required this.apiClient, + required this.refreshParent, + required this.initiallyExpanded, + } + ); + + Future _editMeal(context) async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MealScreen( + apiClient: apiClient, + meal: meal, + refresh: refreshParent, + ), + ), + ).then((_) => refreshParent()); + } + + Future _editEntry(context, entry) async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EditEntryScreen( + apiClient: apiClient, + entry: entry, + ), + ), + ).then((_) => refreshParent()); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + + var width_avail = MediaQuery.of(context).size.width; + var width = width_avail > MAX_WIDTH ? MAX_WIDTH : width_avail; + + return Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: Card( + elevation: 4, + clipBehavior: Clip.antiAlias, + shadowColor: colorScheme.primary.withOpacity(1.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + child: SizedBox( + width: width, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + colorScheme.primary.withOpacity(0.6), + colorScheme.secondary.withOpacity(0.5), + ], + ), + ), + child: InkWell( + splashColor: Colors.blue.withAlpha(30), + onLongPress: () => _editMeal(context), + child: ExpansionTile( + iconColor: colorScheme.onPrimary, + collapsedIconColor: colorScheme.onPrimary, + initiallyExpanded: initiallyExpanded, + title: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + MealHeader(meal: meal), + MacroHeaderWidget( + calories: true, + ), + MacroEntryWidget( + protein: meal.protein, + carb: meal.carb, + fat: meal.fat, + calories: meal.calories, + ), + ], + ), + ), + children: [ + for (var (i, entry) in meal.entries.indexed) + ListTile( + title: EntryWidget( + entry: entry, + ), + tileColor: i % 2 == 0 ? colorScheme.secondary.withOpacity(0.1): Colors.transparent, + onTap: () => _editEntry(context, entry), + ) + ], + ), + ), + ), + ), + ), + ), + ); } } diff --git a/lib/widgets/summary.dart b/lib/widgets/summary.dart new file mode 100644 index 0000000..d28bd46 --- /dev/null +++ b/lib/widgets/summary.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:fooder/models/diary.dart'; +import 'package:fooder/widgets/macroEntry.dart'; +import 'package:fooder/screens/add_meal.dart'; +import 'package:fooder/client.dart'; +import 'dart:core'; + + +class SummaryHeader extends StatelessWidget { + final Diary diary; + final Function addMeal; + + const SummaryHeader( + {super.key, + required this.addMeal, + required this.diary}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + "Summary", + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + Spacer(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: IconButton( + icon: Icon(Icons.playlist_add_rounded), + iconSize: 32, + color: Theme.of(context).colorScheme.onPrimary, + onPressed: () => addMeal(context), + ), + ), + ], + ); + } +} + + +class SummaryWidget extends StatelessWidget { + static final MAX_WIDTH = 920.0; + + final Diary diary; + final ApiClient apiClient; + final Function() refreshParent; + + const SummaryWidget( + {super.key, + required this.diary, + required this.apiClient, + required this.refreshParent}); + + Future _addMeal(context) async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddMealScreen( + apiClient: apiClient, + diary: diary, + ), + ), + ).then((_) => refreshParent()); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + + var width_avail = MediaQuery.of(context).size.width; + var width = width_avail > MAX_WIDTH ? MAX_WIDTH : width_avail; + + return Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: Card( + elevation: 4, + clipBehavior: Clip.antiAlias, + shadowColor: colorScheme.primary.withOpacity(1.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + child: SizedBox( + width: width, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + colorScheme.primary.withOpacity(0.6), + colorScheme.secondary.withOpacity(0.5), + ], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), + child: Column( + children: [ + SummaryHeader(diary: diary, addMeal: _addMeal), + MacroHeaderWidget( + calories: true, + ), + MacroEntryWidget( + protein: diary.protein, + carb: diary.carb, + fat: diary.fat, + calories: diary.calories, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 063ef84..f003e88 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + blur: + dependency: "direct main" + description: + name: blur + sha256: fd23f1247faee4a7d1a3efb6b7c3cea134f3b939d72e5f8d45233deb0776259f + url: "https://pub.dev" + source: hosted + version: "3.1.0" boolean_selector: dependency: transitive description: @@ -41,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -168,6 +184,14 @@ packages: description: flutter source: sdk version: "0.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" http: dependency: "direct main" description: @@ -495,4 +519,4 @@ packages: version: "1.0.4" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.0" + flutter: ">=3.19.2" diff --git a/pubspec.yaml b/pubspec.yaml index 4a5f489..64e6a1a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,7 +39,9 @@ dependencies: intl: ^0.19.0 flutter_secure_storage: ^9.0.0 simple_barcode_scanner: ^0.1.1 + google_fonts: ^6.2.1 flex_color_scheme: ^7.3.1 + blur: ^3.1.0 dev_dependencies: flutter_test: