[UI craze] begin implementing new UI

This commit is contained in:
Piotr Domański 2024-03-28 16:25:49 +01:00
parent 34786d5de3
commit 90fad4a0ac
13 changed files with 474 additions and 153 deletions

View file

@ -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);
}

View file

@ -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,
),
),
),
),
)
);
}
}

View file

@ -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: <Widget>[
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<FDatePickerWidget> createState() => _FDatePickerWidgetState();
}
class _FDatePickerWidgetState extends State<FDatePickerWidget> {
DateTime date = DateTime.now();
@override
void initState() {
super.initState();
setState(() {
date = widget.date;
});
}
Future<void> 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: <Widget>[
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)));
},
),
),
],
),
),
);
}
}

View file

@ -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<Path> {
@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;
}

39
lib/components/text.dart Normal file
View file

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class FTextInput extends StatelessWidget {
final String labelText;
final double padding;
final TextEditingController controller;
final List<String>? 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,
),
);
}
}

View file

@ -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',

View file

@ -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<AddEntryScreen> {
@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),

View file

@ -11,5 +11,36 @@ abstract class BasedScreen extends StatefulWidget {
final ApiClient apiClient;
const BasedScreen({super.key, required this.apiClient});
}
abstract class BasedState<T extends BasedScreen> extends State<T> {
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,
),
);
}
}

View file

@ -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<LoginScreen> createState() => _LoginScreen();
}
class _LoginScreen extends State<LoginScreen> {
class _LoginScreen extends BasedState<LoginScreen> {
final usernameController = TextEditingController();
final passwordController = TextEditingController();
@ -22,24 +24,6 @@ class _LoginScreen extends State<LoginScreen> {
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<LoginScreen> {
@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<LoginScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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(
FTextInput(
labelText: 'Password',
),
controller: passwordController,
onFieldSubmitted: (_) => _login(),
autofillHints: const [AutofillHints.password],
obscureText: true,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: FilledButton(
FButton(
labelText: 'Sign In',
onPressed: _login,
child: const Text('Login'),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
padding: const EdgeInsets.symmetric(vertical: 8),
child: TextButton(
onPressed: () {
Navigator.push(

View file

@ -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<MainScreen> createState() => _MainScreen();
}
class _MainScreen extends State<MainScreen> {
class _MainScreen extends BasedState<MainScreen> {
Diary? diary;
DateTime date = DateTime.now();
@ -24,19 +28,18 @@ class _MainScreen extends State<MainScreen> {
Future<void> _asyncInitState() async {
var diaryMap = await widget.apiClient.getDiary(date: date);
setState(() {
diary = Diary.fromJson(diaryMap);
date = date;
});
}
Future<void> _pickDate() async {
date = (await showDatePicker(
context: context,
initialDate: date,
firstDate: DateTime(2020),
lastDate: DateTime(DateTime.now().year + 1),
))!;
Future<void> _pickDate(DateTime date) async {
setState(() {
this.date = date;
});
await _asyncInitState();
}
@ -66,60 +69,34 @@ class _MainScreen extends State<MainScreen> {
Widget title;
if (diary != null) {
content = Container(
constraints: const BoxConstraints(maxWidth: 720),
padding: const EdgeInsets.all(10),
child: DiaryWidget(
diary: diary!,
content = CustomScrollView(
slivers: <Widget>[
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),
);
title = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextButton(
child: Text(
"🅵🅾🅾🅳🅴🆁",
style: logoStyle(context),
),
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,
refreshParent: _asyncInitState,
),
],
),
),
]
);
} 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),

View file

@ -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<RegisterScreen> createState() => _RegisterScreen();
}
class _RegisterScreen extends State<RegisterScreen> {
class _RegisterScreen extends BasedState<RegisterScreen> {
final usernameController = TextEditingController();
final passwordController = TextEditingController();
final passwordConfirmController = TextEditingController();
@ -35,24 +37,6 @@ class _RegisterScreen extends State<RegisterScreen> {
});
}
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<RegisterScreen> {
@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<RegisterScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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(
FTextInput(
labelText: 'Password',
),
controller: passwordController,
autofillHints: const [AutofillHints.password],
),
TextFormField(
obscureText: true,
decoration: const InputDecoration(
labelText: 'Confirm password',
),
FTextInput(
labelText: 'Confirm password',
controller: passwordConfirmController,
autofillHints: const [AutofillHints.password],
onFieldSubmitted: (_) => _register()),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: FilledButton(
onFieldSubmitted: (_) => _register(),
obscureText: true,
),
FButton(
labelText: 'Register account',
onPressed: _register,
child: const Text('Register'),
)),
),
],
),
),

View file

@ -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

View file

@ -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: