diff --git a/lib/components/appBar.dart b/lib/components/appBar.dart new file mode 100644 index 0000000..ce8e16c --- /dev/null +++ b/lib/components/appBar.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class FAppBar extends StatelessWidget implements PreferredSizeWidget { + const FAppBar({super.key}); + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + ) + ); + } + + @override + Size get preferredSize => Size.fromHeight(56); +} diff --git a/lib/components/button.dart b/lib/components/button.dart new file mode 100644 index 0000000..feb43e1 --- /dev/null +++ b/lib/components/button.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class FButton extends StatelessWidget { + final String labelText; + final double padding; + final double insidePadding; + final double fontSize; + final Function()? onPressed; + + const FButton({super.key, required this.labelText, this.padding = 8, this.insidePadding = 24, this.fontSize = 20, this.onPressed}); + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + + return GestureDetector( + onTap: onPressed, + child: Padding( + padding: EdgeInsets.symmetric(vertical: padding, horizontal: padding), + child: Container( + padding: EdgeInsets.symmetric(vertical: insidePadding), + decoration: BoxDecoration( + color: colorScheme.primary, + borderRadius: BorderRadius.circular(4), + ), + child: Center( + child: Text( + labelText, + style: theme.textTheme.button!.copyWith( + fontWeight: FontWeight.bold, + fontSize: fontSize, + ), + ), + ), + ), + ) + ); + } +} diff --git a/lib/components/datePicker.dart b/lib/components/datePicker.dart new file mode 100644 index 0000000..1033312 --- /dev/null +++ b/lib/components/datePicker.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; + + +class FDateItemWidget extends StatelessWidget { + final DateTime date; + final bool picked; + final Function(DateTime) onDatePicked; + + const FDateItemWidget({super.key, required this.date, required this.onDatePicked, this.picked = false}); + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + + var dayOfTheWeekMap = { + 1: 'Mon', + 2: 'Tue', + 3: 'Wed', + 4: 'Thu', + 5: 'Fri', + 6: 'Sat', + 7: 'Sun', + }; + + return GestureDetector( + onTap: () { + onDatePicked(date); + }, + child: Container( + width: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + border: Border.all( + color: colorScheme.onPrimary, + width: 2, + ), + color: picked ? colorScheme.onPrimary.withOpacity(0.25) : Colors.transparent, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + dayOfTheWeekMap[date.weekday]!, + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + Text( + '${date.day}.${date.month}', + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + } +} + +class FDatePickerWidget extends StatefulWidget { + final DateTime date; + final Function(DateTime) onDatePicked; + + const FDatePickerWidget({super.key, required this.date, required this.onDatePicked}); + + @override + State createState() => _FDatePickerWidgetState(); +} + +class _FDatePickerWidgetState extends State { + DateTime date = DateTime.now(); + + @override + void initState() { + super.initState(); + setState(() { + date = widget.date; + }); + } + + Future onDatePicked(DateTime date) async { + setState(() { + this.date = date; + }); + + await widget.onDatePicked(date); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + + return SizedBox( + height: 100, + child: Center( + child: ListView( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + children: [ + SizedBox(width: 25), + 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, + child: IconButton( + icon: Icon( + Icons.calendar_month, + color: colorScheme.onPrimary, + size: 40, + ), + onPressed: () { + onDatePicked(date.add(Duration(days: 1))); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/sliver.dart b/lib/components/sliver.dart new file mode 100644 index 0000000..ac1e21d --- /dev/null +++ b/lib/components/sliver.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; + +class BackgroundWave extends StatelessWidget { + final double height; + + const BackgroundWave({Key? key, required this.height}) : super(key: key); + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + + return SizedBox( + height: height, + child: ClipPath( + clipper: BackgroundWaveClipper(), + child: Container( + width: MediaQuery.of(context).size.width, + height: height, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [colorScheme.primary, colorScheme.secondary], + )), + )), + ); + } +} + +class BackgroundWaveClipper extends CustomClipper { + @override + Path getClip(Size size) { + var path = Path(); + path.lineTo(0.0, size.height); + + var firstCurve = Offset(0, size.height - 20); + var lastCurve = Offset(30, 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); + + path.quadraticBezierTo( + firstCurve.dx, firstCurve.dy, lastCurve.dx, lastCurve.dy, + ); + + firstCurve = Offset(size.width, size.height - 20); + lastCurve = Offset(size.width, size.height); + + path.quadraticBezierTo( + firstCurve.dx, firstCurve.dy, lastCurve.dx, lastCurve.dy, + ); + + path.lineTo(size.width, 0.0); + path.close(); + + return path; + } + + @override + bool shouldReclip(BackgroundWaveClipper oldClipper) => oldClipper != this; +} + +class FSliverAppBar extends SliverPersistentHeaderDelegate { + final Widget child; + const FSliverAppBar({required this.child}); + + @override + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { + var adjustedShrinkOffset = shrinkOffset > minExtent ? minExtent : shrinkOffset; + double offset = (minExtent - adjustedShrinkOffset); + + return Stack( + children: [ + const BackgroundWave( + height: 280, + ), + Positioned( + top: offset + 8, + child: child, + left: 16, + right: 16, + ) + ], + ); + } + + @override + double get maxExtent => 280; + + @override + double get minExtent => 140; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => + oldDelegate.maxExtent != maxExtent || oldDelegate.minExtent != minExtent; +} diff --git a/lib/components/text.dart b/lib/components/text.dart new file mode 100644 index 0000000..10dc877 --- /dev/null +++ b/lib/components/text.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class FTextInput extends StatelessWidget { + final String labelText; + final double padding; + final TextEditingController controller; + final List? autofillHints; + final bool autofocus; + final bool obscureText; + final Function(String)? onFieldSubmitted; + + const FTextInput({super.key, required this.labelText, this.padding = 8, required this.controller, this.autofillHints, this.autofocus = false, this.onFieldSubmitted, this.obscureText = false}); + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + + return Padding( + padding: EdgeInsets.symmetric(vertical: padding, horizontal: padding), + child: TextFormField( + obscureText: obscureText, + decoration: InputDecoration( + labelText: labelText, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: colorScheme.primary), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: colorScheme.onPrimary), + ), + ), + controller: controller, + autofillHints: autofillHints, + autofocus: autofocus, + onFieldSubmitted: onFieldSubmitted, + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index cc495da..785f740 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:fooder/screens/login.dart'; import 'package:fooder/client.dart'; +import 'package:flex_color_scheme/flex_color_scheme.dart'; class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -9,13 +10,10 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'FOODER', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.blueGrey, - brightness: Brightness.dark, - ), - useMaterial3: true, - ), + theme: FlexThemeData.light(scheme: FlexScheme.brandBlue), + darkTheme: FlexThemeData.dark(scheme: FlexScheme.brandBlue), + themeMode: ThemeMode.system, + debugShowCheckedModeBanner: false, home: LoginScreen( apiClient: ApiClient( baseUrl: 'https://fooderapi.domandoman.xyz/api', diff --git a/lib/screens/add_entry.dart b/lib/screens/add_entry.dart index 2a5429b..c9d94f2 100644 --- a/lib/screens/add_entry.dart +++ b/lib/screens/add_entry.dart @@ -7,6 +7,8 @@ import 'package:fooder/models/meal.dart'; import 'package:fooder/widgets/product.dart'; import 'package:fooder/screens/add_product.dart'; import 'package:simple_barcode_scanner/simple_barcode_scanner.dart'; +import 'package:fooder/components/appBar.dart'; + class AddEntryScreen extends BasedScreen { final Diary diary; @@ -126,10 +128,8 @@ class _AddEntryScreen extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text("πŸ…΅πŸ…ΎπŸ…ΎπŸ…³πŸ…΄πŸ†", style: logoStyle(context)), - ), + extendBodyBehindAppBar: true, + appBar: FAppBar(), body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 720), diff --git a/lib/screens/based.dart b/lib/screens/based.dart index 5c776c2..f725e2a 100644 --- a/lib/screens/based.dart +++ b/lib/screens/based.dart @@ -11,5 +11,36 @@ abstract class BasedScreen extends StatefulWidget { final ApiClient apiClient; const BasedScreen({super.key, required this.apiClient}); - +} + +abstract class BasedState extends State { + void showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + message, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + + void showText(String text) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + text, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + ); + } } diff --git a/lib/screens/login.dart b/lib/screens/login.dart index 5c272fe..2ad8f60 100644 --- a/lib/screens/login.dart +++ b/lib/screens/login.dart @@ -3,6 +3,8 @@ import 'package:flutter/services.dart'; import 'package:fooder/screens/based.dart'; import 'package:fooder/screens/main.dart'; import 'package:fooder/screens/register.dart'; +import 'package:fooder/components/text.dart'; +import 'package:fooder/components/button.dart'; class LoginScreen extends BasedScreen { const LoginScreen({super.key, required super.apiClient}); @@ -11,7 +13,7 @@ class LoginScreen extends BasedScreen { State createState() => _LoginScreen(); } -class _LoginScreen extends State { +class _LoginScreen extends BasedState { final usernameController = TextEditingController(); final passwordController = TextEditingController(); @@ -22,24 +24,6 @@ class _LoginScreen extends State { super.dispose(); } - void showError(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message, textAlign: TextAlign.center), - backgroundColor: Theme.of(context).colorScheme.error, - ), - ); - } - - void showText(String text) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(text, textAlign: TextAlign.center), - backgroundColor: Theme.of(context).colorScheme.primary, - ), - ); - } - void popMeDaddy() { Navigator.pushReplacement( context, @@ -95,11 +79,10 @@ class _LoginScreen extends State { @override Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text("πŸ…΅πŸ…ΎπŸ…ΎπŸ…³πŸ…΄πŸ†", style: logoStyle(context)), - ), body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 600), @@ -108,32 +91,30 @@ class _LoginScreen extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - TextFormField( - decoration: const InputDecoration( - labelText: 'Username', - ), + Icon( + Icons.lock, + size: 100, + color: colorScheme.primary, + ), + FTextInput( + labelText: 'Username', controller: usernameController, autofillHints: const [AutofillHints.username], autofocus: true, ), - TextFormField( - obscureText: true, - decoration: const InputDecoration( - labelText: 'Password', - ), + FTextInput( + labelText: 'Password', controller: passwordController, onFieldSubmitted: (_) => _login(), autofillHints: const [AutofillHints.password], + obscureText: true, + ), + FButton( + labelText: 'Sign In', + onPressed: _login, ), Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: FilledButton( - onPressed: _login, - child: const Text('Login'), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.symmetric(vertical: 8), child: TextButton( onPressed: () { Navigator.push( diff --git a/lib/screens/main.dart b/lib/screens/main.dart index ac49118..9c93fea 100644 --- a/lib/screens/main.dart +++ b/lib/screens/main.dart @@ -4,6 +4,10 @@ import 'package:fooder/screens/login.dart'; import 'package:fooder/screens/add_entry.dart'; import 'package:fooder/models/diary.dart'; import 'package:fooder/widgets/diary.dart'; +import 'package:fooder/widgets/meal.dart'; +import 'package:fooder/components/appBar.dart'; +import 'package:fooder/components/sliver.dart'; +import 'package:fooder/components/datePicker.dart'; class MainScreen extends BasedScreen { const MainScreen({super.key, required super.apiClient}); @@ -12,7 +16,7 @@ class MainScreen extends BasedScreen { State createState() => _MainScreen(); } -class _MainScreen extends State { +class _MainScreen extends BasedState { Diary? diary; DateTime date = DateTime.now(); @@ -24,19 +28,18 @@ class _MainScreen extends State { Future _asyncInitState() async { var diaryMap = await widget.apiClient.getDiary(date: date); + setState(() { diary = Diary.fromJson(diaryMap); date = date; }); } - Future _pickDate() async { - date = (await showDatePicker( - context: context, - initialDate: date, - firstDate: DateTime(2020), - lastDate: DateTime(DateTime.now().year + 1), - ))!; + Future _pickDate(DateTime date) async { + setState(() { + this.date = date; + }); + await _asyncInitState(); } @@ -66,60 +69,34 @@ class _MainScreen extends State { Widget title; if (diary != null) { - content = Container( - constraints: const BoxConstraints(maxWidth: 720), - padding: const EdgeInsets.all(10), - child: DiaryWidget( - diary: diary!, - apiClient: widget.apiClient, - refreshParent: _asyncInitState), - ); - title = Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - child: Text( - "πŸ…΅πŸ…ΎπŸ…ΎπŸ…³πŸ…΄πŸ†", - style: logoStyle(context), + content = CustomScrollView( + slivers: [ + SliverPersistentHeader( + delegate: FSliverAppBar(child: FDatePickerWidget(date: date, onDatePicked: _pickDate)), + pinned: true, + ), + SliverList( + delegate: SliverChildListDelegate( + [ + for (var meal in diary!.meals) + MealWidget( + meal: meal, + apiClient: widget.apiClient, + refreshParent: _asyncInitState, + ), + ], ), - onPressed: () { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => - MainScreen(apiClient: widget.apiClient)), - ).then((_) => _asyncInitState()); - }, ), - const Spacer(), - Text( - "${date.year}-${date.month}-${date.day}", - style: const TextStyle(fontSize: 20), - ), - IconButton( - icon: const Icon(Icons.calendar_month), - onPressed: _pickDate, - ), - const Spacer(), - IconButton( - icon: const Icon(Icons.logout), - onPressed: _logout, - ), - ], + ] ); } else { - content = const CircularProgressIndicator(); - title = Text("πŸ…΅πŸ…ΎπŸ…ΎπŸ…³πŸ…΄πŸ†", style: logoStyle(context)); + content = const Center(child: const CircularProgressIndicator()); } return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: title, - ), - body: Center( - child: content, - ), + body: content, + extendBodyBehindAppBar: true, + appBar: FAppBar(), floatingActionButton: FloatingActionButton( onPressed: _addEntry, child: const Icon(Icons.add), diff --git a/lib/screens/register.dart b/lib/screens/register.dart index ece6a1a..32d7eb2 100644 --- a/lib/screens/register.dart +++ b/lib/screens/register.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fooder/screens/based.dart'; +import 'package:fooder/components/text.dart'; +import 'package:fooder/components/button.dart'; class RegisterScreen extends BasedScreen { const RegisterScreen({super.key, required super.apiClient}); @@ -9,7 +11,7 @@ class RegisterScreen extends BasedScreen { State createState() => _RegisterScreen(); } -class _RegisterScreen extends State { +class _RegisterScreen extends BasedState { final usernameController = TextEditingController(); final passwordController = TextEditingController(); final passwordConfirmController = TextEditingController(); @@ -35,24 +37,6 @@ class _RegisterScreen extends State { }); } - void showError(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message, textAlign: TextAlign.center), - backgroundColor: Theme.of(context).colorScheme.error, - ), - ); - } - - void showText(String text) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(text, textAlign: TextAlign.center), - backgroundColor: Theme.of(context).colorScheme.primary, - ), - ); - } - void popMeDaddy() { Navigator.pop(context); } @@ -78,11 +62,10 @@ class _RegisterScreen extends State { @override Widget build(BuildContext context) { + var theme = Theme.of(context); + var colorScheme = theme.colorScheme; + return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text("πŸ…΅πŸ…ΎπŸ…ΎπŸ…³πŸ…΄πŸ†", style: logoStyle(context)), - ), body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 600), @@ -91,35 +74,34 @@ class _RegisterScreen extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - TextFormField( - decoration: const InputDecoration( - labelText: 'Username', - ), + Icon( + Icons.group_add, + size: 100, + color: colorScheme.primary, + ), + FTextInput( + labelText: 'Username', controller: usernameController, autofillHints: const [AutofillHints.username], + autofocus: true, ), - TextFormField( - obscureText: true, - decoration: const InputDecoration( - labelText: 'Password', - ), + FTextInput( + labelText: 'Password', controller: passwordController, autofillHints: const [AutofillHints.password], + obscureText: true, + ), + FTextInput( + labelText: 'Confirm password', + controller: passwordConfirmController, + autofillHints: const [AutofillHints.password], + onFieldSubmitted: (_) => _register(), + obscureText: true, + ), + FButton( + labelText: 'Register account', + onPressed: _register, ), - TextFormField( - obscureText: true, - decoration: const InputDecoration( - labelText: 'Confirm password', - ), - controller: passwordConfirmController, - autofillHints: const [AutofillHints.password], - onFieldSubmitted: (_) => _register()), - Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: FilledButton( - onPressed: _register, - child: const Text('Register'), - )), ], ), ), diff --git a/pubspec.lock b/pubspec.lock index 6554a65..063ef84 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + flex_color_scheme: + dependency: "direct main" + description: + name: flex_color_scheme + sha256: "32914024a4f404d90ff449f58d279191675b28e7c08824046baf06826e99d984" + url: "https://pub.dev" + source: hosted + version: "7.3.1" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" + url: "https://pub.dev" + source: hosted + version: "1.4.0" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 897143a..4a5f489 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: intl: ^0.19.0 flutter_secure_storage: ^9.0.0 simple_barcode_scanner: ^0.1.1 + flex_color_scheme: ^7.3.1 dev_dependencies: flutter_test: