Overhaul ui and remove BackButtonListener #9

Merged
marco merged 1 commits from home-button-in-drawer into master 2025-01-05 16:27:58 +00:00
20 changed files with 744 additions and 425 deletions

View File

@ -48,6 +48,10 @@ android {
}
buildTypes {
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
}
release {
signingConfig = signingConfigs.release
}

View File

@ -0,0 +1,59 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'package:calorimeter/main.dart';
import 'package:calorimeter/storage/storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUp(() {});
group('end-to-end test', () {
testWidgets('add food manually', (tester) async {
var foodStorage = await FoodStorage.create();
await tester.pumpWidget(MainApp(storage: foodStorage));
await tester.pumpAndSettle();
final addButtonFinder = find.byIcon(Icons.add);
expect(addButtonFinder, findsOneWidget);
await tester.tap(addButtonFinder);
await tester.pumpAndSettle();
final nameAutocompleteFinder =
find.widgetWithText(Autocomplete<String>, "Name");
final amountFinder = find.widgetWithText(TextField, "Amount");
final kcalFinder = find.widgetWithText(TextField, "kcal");
final addButton = find.widgetWithIcon(ElevatedButton, Icons.check);
expect(nameAutocompleteFinder, findsOneWidget);
expect(amountFinder, findsOneWidget);
expect(kcalFinder, findsOneWidget);
expect(addButton, findsOneWidget);
await tester.enterText(nameAutocompleteFinder, "Bread");
await tester.enterText(amountFinder, "150");
await tester.enterText(kcalFinder, "250");
await tester.tap(addButton);
await tester.pumpAndSettle();
// EnterFoodWidget collapses
expect(nameAutocompleteFinder, findsNothing);
var enteredFood = find.text("Bread");
var enteredAmount = find.text("150");
var enteredKcal = find.text("250");
await tester.pumpAndSettle();
expect(enteredFood, findsOneWidget);
expect(enteredAmount, findsOneWidget);
expect(enteredKcal, findsOneWidget);
});
});
}

View File

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'package:calorimeter/storage/storage.dart';
import 'package:flutter/material.dart';
import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/utils/row_with_spacers_widget.dart';
@ -8,8 +8,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class EnterFoodWidget extends StatefulWidget {
final Function(BuildContext context, FoodEntryState entry) onAdd;
final Map<String, int> foodEntryLookupDatabase;
const EnterFoodWidget({super.key, required this.onAdd});
const EnterFoodWidget(
{super.key, required this.onAdd, required this.foodEntryLookupDatabase});
@override
State<EnterFoodWidget> createState() => _EnterFoodWidgetState();
@ -19,28 +21,52 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
late TextEditingController nameController;
late TextEditingController massController;
late TextEditingController kcalPerMassController;
late Map<String, int> suggestions;
late bool open;
@override
void initState() {
nameController = TextEditingController();
massController = TextEditingController();
kcalPerMassController = TextEditingController();
suggestions = FoodStorage.getInstance().getFoodEntryLookupDatabase;
open = false;
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
return Column(
children: [
Stack(
children: [
if (!open)
RowWidget(
showDividers: false,
null,
null,
null,
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
),
onPressed: () => setState(() => open = true),
child: Icon(Icons.add)),
),
Offstage(
offstage: !open,
child: AnimatedOpacity(
duration: Duration(milliseconds: 250),
opacity: open ? 1.0 : 0.0,
child: RowWidget(
showDividers: true,
Autocomplete<String>(
optionsViewOpenDirection: OptionsViewOpenDirection.down,
fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
fieldViewBuilder:
(context, controller, focusNode, onSubmitted) {
nameController = controller;
return TextFormField(
scrollPadding: EdgeInsets.only(bottom: 100),
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
@ -53,7 +79,7 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
return const Iterable<String>.empty();
}
return suggestions.keys.where(
return widget.foodEntryLookupDatabase.keys.where(
(name) {
return name
.toLowerCase()
@ -62,7 +88,8 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
);
},
onSelected: (selectedFood) {
int kcalPerMassForSelectedFood = suggestions[selectedFood]!;
int kcalPerMassForSelectedFood =
widget.foodEntryLookupDatabase[selectedFood]!;
setState(() {
nameController.text = selectedFood;
kcalPerMassController.text =
@ -70,11 +97,12 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
});
}),
TextField(
scrollPadding: EdgeInsets.only(bottom: 100),
textAlign: TextAlign.end,
decoration: InputDecoration(
label: Align(
alignment: Alignment.centerRight,
child: Text(AppLocalizations.of(context)!.amountPer),
label: Directionality(
textDirection: TextDirection.rtl,
child: Text(AppLocalizations.of(context)!.amount),
),
),
keyboardType: TextInputType.number,
@ -82,26 +110,34 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
onSubmitted: (value) => onSubmitAction(),
),
TextField(
scrollPadding: EdgeInsets.only(bottom: 100),
textAlign: TextAlign.end,
decoration: InputDecoration(
label: Align(
alignment: Alignment.centerRight,
child: Text(AppLocalizations.of(context)!.kcalper),
)),
label: Directionality(
textDirection: TextDirection.rtl,
child: Text(AppLocalizations.of(context)!.kcal))),
keyboardType: TextInputType.number,
controller: kcalPerMassController,
onSubmitted: (value) => onSubmitAction(),
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: ElevatedButton(
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
),
onPressed: () => onSubmitAction(),
child: const Icon(Icons.add)),
child: const Icon(Icons.check)),
),
),
),
],
),
SizedBox(
height: 200,
child: GestureDetector(
onTap: () => setState(() {
open = false;
}))),
],
);
}
@ -143,6 +179,7 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
nameController.text = "";
massController.text = "";
kcalPerMassController.text = "";
open = false;
});
}
}

View File

@ -40,12 +40,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(event.entry);
// this is just checking if writing to the database worked
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries));
}
@ -65,12 +61,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(event.newEntry);
// this is just checking if writing to the database worked
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries));
}
@ -84,12 +76,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
await storage.writeEntriesForDate(event.forDate, entriesForDate);
// this is just checking if writing to the database worked
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries));
}
@ -109,11 +97,10 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
return;
}
var client = FoodFactLookupClient();
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var client = FoodFactLookupClient();
if (scanResult.type == ResultType.Cancelled) {
return;
}
@ -134,9 +121,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
);
entriesForDate.add(newEntryWaiting);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries));
state.foodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: state.foodEntries));
await responseFuture.then((response) async {
var index = entriesForDate
@ -184,9 +170,8 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting);
var entriesFromStorage = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesFromStorage});
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries));
});

View File

@ -1,8 +1,8 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'package:flutter/material.dart';
import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/utils/row_with_spacers_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class FoodEntryWidget extends StatefulWidget {
@ -24,6 +24,8 @@ class FoodEntryWidget extends StatefulWidget {
}
class _FoodEntryWidgetState extends State<FoodEntryWidget> {
final animationDuration = const Duration(milliseconds: 150);
@override
void initState() {
super.initState();
@ -32,56 +34,61 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => widget.onTap(context, widget.entry),
child: Stack(
children: [
Positioned.fill(
onTap: () {
widget.onTap(context, widget.entry);
},
child: Stack(children: [
Positioned.fill(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: RowWidget(
RowWidget(
showDividers: !widget.entry.isSelected,
widget.entry.waitingForNetwork
? const Center(child: CircularProgressIndicator())
: Text(widget.entry.name),
widget.entry.waitingForNetwork
AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 0.0 : 1.0,
child: widget.entry.waitingForNetwork
? Container()
: Text(widget.entry.mass.ceil().toString(),
textAlign: TextAlign.end),
Opacity(
),
AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 0.0 : 1.0,
child: widget.entry.waitingForNetwork
? Container()
: Text(widget.entry.kcalPer100.ceil().toString(),
textAlign: TextAlign.end),
),
Opacity(
AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 0.0 : 1.0,
child: widget.entry.waitingForNetwork
? Container()
: Text(
(widget.entry.mass *
widget.entry.kcalPer100 /
100)
(widget.entry.mass * widget.entry.kcalPer100 / 100)
.ceil()
.toString(),
textAlign: TextAlign.end),
),
),
),
),
Opacity(
Positioned.fill(
child: Stack(children: [
AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 0.66 : 0.0,
child: Container(
color: Theme.of(context).colorScheme.secondary)),
]),
color: Theme.of(context).colorScheme.secondary,
),
Opacity(
),
AnimatedOpacity(
duration: animationDuration,
opacity: widget.entry.isSelected ? 1.0 : 0.0,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
width: 64,
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(Icons.edit),
@ -94,29 +101,27 @@ class _FoodEntryWidgetState extends State<FoodEntryWidget> {
return FoodEntryChangeDialog(
entry: widget.entry,
onChange: (context, entry) {
widget.onChange(context, entry);
widget.onChange(
context, entry);
});
});
},
);
}
: null),
),
: null)),
SizedBox(
width: 64,
child: IconButton(
padding: const EdgeInsets.all(0.0),
iconSize: 24,
icon: const Icon(Icons.delete),
icon: const Icon(
Icons.delete,
color: Colors.redAccent,
),
onPressed: widget.entry.isSelected
? () => widget.onDelete(context, widget.entry.id)
: null),
),
],
),
),
],
),
);
? () =>
widget.onDelete(context, widget.entry.id)
: null))
])))
]))
]));
}
}

View File

@ -77,19 +77,23 @@ class FoodFactModel {
}
}
String quantityString = json['product']['product_quantity'] ?? "0";
double quantity;
int quantityForModel = 0;
try {
quantity = double.parse(quantityString);
String quantityString = json['product']['product_quantity'] ?? "0";
quantityForModel = double.parse(quantityString).ceil();
} catch (e) {
quantity = 0;
try {
quantityForModel =
(json['product']['product_quantity'] as num).toDouble().ceil();
} catch (e) {
quantityForModel = 0;
}
}
return FoodFactModel(
name: json['product']['product_name'] ?? "",
kcalPer100g: kcalPer100gForModel,
mass: quantity.ceil(),
mass: quantityForModel,
);
}
}

View File

@ -1,9 +1,12 @@
{
"today": "Heute",
"ok": "OK",
"name": "Name",
"amount": "Menge",
"amountPer": "Menge in 100 g/ml",
"kcalper": "kcal pro 100 g/ml",
"amountPer": "Menge in g oder ml",
"kcal": "kcal",
"kcalper": "kcal pro 100 g oder ml",
"kcalSum": "kcal gesamt",
"kcalToday": "kcal heute",
"menu": "Menü",
"settings": "Einstellungen",

View File

@ -1,9 +1,12 @@
{
"today": "Today",
"ok": "OK",
"name": "Name",
"amount": "Amount",
"amountPer": "Amount in 100 g/ml",
"kcalper": "kcal per 100 g/ml",
"amountPer": "Amount in g or ml",
"kcal": "kcal",
"kcalper": "kcal per 100 g or ml",
"kcalSum": "kcal total",
"kcalToday": "kcal today",
"menu": "Menu",
"settings": "Settings",

View File

@ -1,5 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
/* Copyright (C) 2024 Marco Groß <mgross@sw-gross.de> */
import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/perdate/perdate_pageview_controller.dart';
import 'package:calorimeter/storage/storage.dart';
@ -8,65 +9,81 @@ import 'package:calorimeter/utils/settings_bloc.dart';
import 'package:calorimeter/utils/theme_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
List<FoodEntryState> entriesForToday = [];
DateTime timeNow = DateTime.now();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
var storage = await FoodStorage.create();
await storage.buildFoodLookupDatabase();
timeNow = DateTimeHelper.now();
entriesForToday = await storage.getEntriesForDate(timeNow);
var kcalLimit = await storage.readLimit();
var brightness = await storage.readBrightness();
var foodStorage = await FoodStorage.create();
await foodStorage.buildFoodLookupDatabase();
</