Compare commits

..

No commits in common. "master" and "swipeable-pages" have entirely different histories.

5 changed files with 140 additions and 204 deletions

View File

@ -4,96 +4,61 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:calorimeter/storage/storage.dart'; import 'package:calorimeter/storage/storage.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> { class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
final GlobalEntryState initialState; final PageState initialState;
final FoodStorage storage; final FoodStorage storage;
final DateTime forDate;
FoodEntryBloc({required this.initialState, required this.storage}) FoodEntryBloc(
{required this.initialState,
required this.forDate,
required this.storage})
: super(initialState) { : super(initialState) {
on<PageBeingInitialized>(handlePageBeingInitialized);
on<FoodEntryEvent>(handleFoodEntryEvent); on<FoodEntryEvent>(handleFoodEntryEvent);
on<FoodChangedEvent>(handleFoodChangedEvent); on<FoodChangedEvent>(handleFoodChangedEvent);
on<FoodDeletionEvent>(handleDeleteFoodEvent); on<FoodDeletionEvent>(handleDeleteFoodEvent);
on<BarcodeScanned>(handleBarcodeScannedEvent); on<BarcodeScanned>(handleBarcodeScannedEvent);
on<FoodEntryTapped>(handleFoodEntryTapped); on<FoodEntryTapped>(handleFoodEntryTapped);
} }
void handlePageBeingInitialized(
PageBeingInitialized event, Emitter<GlobalEntryState> emit) async {
var newList = await storage.getEntriesForDate(event.forDate);
state.foodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: state.foodEntries));
}
void handleFoodEntryEvent( void handleFoodEntryEvent(
FoodEntryEvent event, Emitter<GlobalEntryState> emit) async { FoodEntryEvent event, Emitter<PageState> emit) async {
var entriesForDate = state.foodEntries[event.forDate]; PageState newState = PageState.from(state);
entriesForDate ??= []; newState.addEntry(event.entry);
entriesForDate.add(event.entry); await storage.writeEntriesForDate(forDate, newState.foodEntries);
await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(event.entry); storage.addFoodEntryToLookupDatabase(event.entry);
// this is just checking if writing to the database worked emit(newState);
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
void handleFoodChangedEvent( void handleFoodChangedEvent(
FoodChangedEvent event, Emitter<GlobalEntryState> emit) async { FoodChangedEvent event, Emitter<PageState> emit) async {
var entriesForDate = state.foodEntries[event.forDate]; var entries = state.foodEntries;
if (entriesForDate == null) return; var index = entries.indexWhere((entry) {
var index = entriesForDate.indexWhere((entry) {
return entry.id == event.newEntry.id; return entry.id == event.newEntry.id;
}); });
entriesForDate.removeAt(index); entries.removeAt(index);
entriesForDate.insert(index, event.newEntry); entries.insert(index, event.newEntry);
await storage.writeEntriesForDate(event.forDate, entriesForDate); await storage.writeEntriesForDate(forDate, entries);
storage.addFoodEntryToLookupDatabase(event.newEntry); storage.addFoodEntryToLookupDatabase(event.newEntry);
// this is just checking if writing to the database worked emit(PageState(foodEntries: entries));
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
void handleDeleteFoodEvent( void handleDeleteFoodEvent(
FoodDeletionEvent event, Emitter<GlobalEntryState> emit) async { FoodDeletionEvent event, Emitter<PageState> emit) async {
var entriesForDate = state.foodEntries[event.forDate]; state.foodEntries.removeWhere((entry) => entry.id == event.entryID);
if (entriesForDate == null) return;
entriesForDate.removeWhere((entry) => entry.id == event.entryID); await storage.writeEntriesForDate(forDate, state.foodEntries);
await storage.writeEntriesForDate(event.forDate, entriesForDate); emit(PageState.from(state));
// 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});
emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
void handleBarcodeScannedEvent( void handleBarcodeScannedEvent(
BarcodeScanned event, Emitter<GlobalEntryState> emit) async { BarcodeScanned event, Emitter<PageState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var client = FoodFactLookupClient(); var client = FoodFactLookupClient();
var scanResult = await event.scanResultFuture; var scanResult = await event.scanResultFuture;
@ -101,13 +66,14 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
return; return;
} }
if (scanResult.type == ResultType.Error) { if (scanResult.type == ResultType.Error) {
emit(GlobalEntryState( emit(PageState(
foodEntries: state.foodEntries, foodEntries: state.foodEntries,
errorString: "Fehler beim Scannen des Barcodes")); errorString: "Fehler beim Scannen des Barcodes"));
return; return;
} }
var responseFuture = client.retrieveFoodInfo(scanResult.rawContent); var responseFuture = client.retrieveFoodInfo(scanResult.rawContent);
List<FoodEntryState> newList = List.from(state.foodEntries);
var newEntryWaiting = FoodEntryState( var newEntryWaiting = FoodEntryState(
kcalPer100: 0, kcalPer100: 0,
name: "", name: "",
@ -115,13 +81,11 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
waitingForNetwork: true, waitingForNetwork: true,
isSelected: false, isSelected: false,
); );
entriesForDate.add(newEntryWaiting); newList.add(newEntryWaiting);
var newFoodEntries = state.foodEntries; emit(PageState(foodEntries: newList));
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries));
await responseFuture.then((response) async { await responseFuture.then((response) async {
var index = entriesForDate var index = newList
.indexWhere((entryState) => entryState.id == newEntryWaiting.id); .indexWhere((entryState) => entryState.id == newEntryWaiting.id);
// element not found (was deleted previously) // element not found (was deleted previously)
@ -130,23 +94,25 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
} }
if (response.status == FoodFactResponseStatus.barcodeNotFound) { if (response.status == FoodFactResponseStatus.barcodeNotFound) {
entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id); List<FoodEntryState> listWithEntryRemoved =
var newFoodEntries = state.foodEntries; List.from(state.foodEntries);
newFoodEntries.addAll({event.forDate: entriesForDate}); listWithEntryRemoved
.removeWhere((entry) => entry.id == newEntryWaiting.id);
emit(GlobalEntryState( emit(PageState(
foodEntries: newFoodEntries, foodEntries: listWithEntryRemoved,
errorString: "Barcode konnte nicht gefunden werden.")); errorString: "Barcode konnte nicht gefunden werden."));
return; return;
} }
if (response.status == if (response.status ==
FoodFactResponseStatus.foodFactServerNotReachable) { FoodFactResponseStatus.foodFactServerNotReachable) {
entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id); List<FoodEntryState> listWithEntryRemoved =
var newFoodEntries = state.foodEntries; List.from(state.foodEntries);
newFoodEntries.addAll({event.forDate: entriesForDate}); listWithEntryRemoved
.removeWhere((entry) => entry.id == newEntryWaiting.id);
emit(GlobalEntryState( emit(PageState(
foodEntries: newFoodEntries, foodEntries: listWithEntryRemoved,
errorString: "OpenFoodFacts-Server konnte nicht erreicht werden.")); errorString: "OpenFoodFacts-Server konnte nicht erreicht werden."));
return; return;
} }
@ -159,114 +125,91 @@ class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
isSelected: false, isSelected: false,
); );
entriesForDate.removeAt(index); newList.removeAt(index);
entriesForDate.insert(index, newEntryFinishedWaiting); newList.insert(index, newEntryFinishedWaiting);
await storage.writeEntriesForDate(event.forDate, entriesForDate); await storage.writeEntriesForDate(forDate, newList);
storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting); storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting);
var entriesFromStorage = await storage.getEntriesForDate(event.forDate); emit(PageState(foodEntries: newList));
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesFromStorage});
emit(GlobalEntryState(foodEntries: newFoodEntries));
}); });
} }
void handleFoodEntryTapped( void handleFoodEntryTapped(
FoodEntryTapped event, Emitter<GlobalEntryState> emit) async { FoodEntryTapped event, Emitter<PageState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var oldStateOfTappedEntry = event.entry.isSelected; var oldStateOfTappedEntry = event.entry.isSelected;
for (var entry in entriesForDate) { for (var entry in state.foodEntries) {
entry.isSelected = false; entry.isSelected = false;
} }
var selectedEntry = entriesForDate.firstWhere((entry) { var selectedEntry = state.foodEntries.firstWhere((entry) {
return entry.id == event.entry.id; return entry.id == event.entry.id;
}); });
selectedEntry.isSelected = !oldStateOfTappedEntry; selectedEntry.isSelected = !oldStateOfTappedEntry;
emit(GlobalEntryState(foodEntries: state.foodEntries)); emit(PageState(foodEntries: state.foodEntries));
} }
} }
class FoodEvent { class FoodEvent {}
final DateTime forDate;
FoodEvent({required this.forDate});
}
class PageBeingInitialized extends FoodEvent {
PageBeingInitialized({required super.forDate});
}
class FoodEntryEvent extends FoodEvent { class FoodEntryEvent extends FoodEvent {
final FoodEntryState entry; final FoodEntryState entry;
FoodEntryEvent({required this.entry, required super.forDate}); FoodEntryEvent({required this.entry});
} }
class FoodChangedEvent extends FoodEvent { class FoodChangedEvent extends FoodEvent {
final FoodEntryState newEntry; final FoodEntryState newEntry;
FoodChangedEvent({required this.newEntry, required super.forDate}); FoodChangedEvent({required this.newEntry});
} }
class FoodDeletionEvent extends FoodEvent { class FoodDeletionEvent extends FoodEvent {
final String entryID; final String entryID;
FoodDeletionEvent({required this.entryID, required super.forDate}); FoodDeletionEvent({required this.entryID});
} }
class BarcodeScanned extends FoodEvent { class BarcodeScanned extends FoodEvent {
final Future<ScanResult> scanResultFuture; final Future<ScanResult> scanResultFuture;
BarcodeScanned({required this.scanResultFuture, required super.forDate}); BarcodeScanned({required this.scanResultFuture});
} }
class FoodEntryTapped extends FoodEvent { class FoodEntryTapped extends FoodEvent {
final FoodEntryState entry; final FoodEntryState entry;
FoodEntryTapped({required this.entry, required super.forDate}); FoodEntryTapped({required this.entry});
} }
/// This is the state for one date/page /// This is the state for one date/page
class GlobalEntryState { class PageState {
final Map<DateTime, List<FoodEntryState>> foodEntries; final List<FoodEntryState> foodEntries;
final String? errorString; final String? errorString;
GlobalEntryState({required this.foodEntries, this.errorString}); PageState({required this.foodEntries, this.errorString});
factory GlobalEntryState.init() { factory PageState.init() {
return GlobalEntryState(foodEntries: {}); return PageState(foodEntries: []);
} }
static from(GlobalEntryState state) { static from(PageState state) {
return GlobalEntryState(foodEntries: state.foodEntries); return PageState(foodEntries: state.foodEntries);
} }
bool addEntry(FoodEntryState entry, DateTime date) { void addEntry(FoodEntryState entry) {
var list = foodEntries[date]; foodEntries.add(entry);
if (list == null) {
return false;
}
list.add(entry);
return true;
} }
} }
class FoodEntryState { class FoodEntryState {
final String id;
final String name; final String name;
final int mass; final int mass;
final int kcalPer100; final int kcalPer100;
final String id;
final bool waitingForNetwork; final bool waitingForNetwork;
bool isSelected; bool isSelected;

View File

@ -1,4 +1,3 @@
import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/perdate/perdate_pageview.dart'; import 'package:calorimeter/perdate/perdate_pageview.dart';
import 'package:calorimeter/storage/storage.dart'; import 'package:calorimeter/storage/storage.dart';
import 'package:calorimeter/utils/settings_bloc.dart'; import 'package:calorimeter/utils/settings_bloc.dart';
@ -8,9 +7,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
List<FoodEntryState> entriesForToday = [];
DateTime timeNow = DateTime.now();
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -18,16 +14,6 @@ void main() async {
var storage = await FoodStorage.create(); var storage = await FoodStorage.create();
await storage.buildFoodLookupDatabase(); await storage.buildFoodLookupDatabase();
timeNow = DateTime.now().copyWith(
hour: 0,
isUtc: true,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0);
entriesForToday = await storage.getEntriesForDate(timeNow);
var kcalLimit = await storage.readLimit(); var kcalLimit = await storage.readLimit();
var brightness = await storage.readBrightness(); var brightness = await storage.readBrightness();
@ -56,13 +42,6 @@ class MainApp extends StatelessWidget {
return SafeArea( return SafeArea(
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [
BlocProvider(
create: (context) => FoodEntryBloc(
storage: storage,
initialState:
GlobalEntryState(foodEntries: {timeNow: entriesForToday}),
),
),
BlocProvider( BlocProvider(
create: (context) => SettingsDataBloc( create: (context) => SettingsDataBloc(
SettingsState(kcalLimit: kcalLimit), SettingsState(kcalLimit: kcalLimit),

View File

@ -6,11 +6,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class FoodEntryList extends StatelessWidget { class FoodEntryList extends StatelessWidget {
final List<FoodEntryState> entries; final List<FoodEntryState> entries;
final DateTime date;
const FoodEntryList({ const FoodEntryList({
required this.entries, required this.entries,
required this.date,
super.key, super.key,
}); });
@ -27,7 +25,7 @@ class FoodEntryList extends StatelessWidget {
onAdd: (context, entry) { onAdd: (context, entry) {
context context
.read<FoodEntryBloc>() .read<FoodEntryBloc>()
.add(FoodEntryEvent(entry: entry, forDate: date)); .add(FoodEntryEvent(entry: entry));
}, },
), ),
const SizedBox(height: 75), const SizedBox(height: 75),
@ -42,19 +40,19 @@ class FoodEntryList extends StatelessWidget {
key: ValueKey(entries[entryIndex].id), key: ValueKey(entries[entryIndex].id),
entry: entries[entryIndex], entry: entries[entryIndex],
onDelete: (_, id) { onDelete: (_, id) {
context context.read<FoodEntryBloc>().add(FoodDeletionEvent(
.read<FoodEntryBloc>() entryID: id,
.add(FoodDeletionEvent(entryID: id, forDate: date)); ));
}, },
onChange: (_, changedEntry) { onChange: (_, changedEntry) {
context.read<FoodEntryBloc>().add( context
FoodChangedEvent(newEntry: changedEntry, forDate: date), .read<FoodEntryBloc>()
); .add(FoodChangedEvent(newEntry: changedEntry));
}, },
onTap: (_, tappedEntry) { onTap: (_, tappedEntry) {
context.read<FoodEntryBloc>().add( context
FoodEntryTapped(entry: tappedEntry, forDate: date), .read<FoodEntryBloc>()
); .add(FoodEntryTapped(entry: tappedEntry));
}, },
), ),
const Divider(), const Divider(),

View File

@ -61,7 +61,7 @@ class _PerDatePageviewState extends State<PerDatePageview> {
return PerDateWidget( return PerDateWidget(
key: ValueKey(dateToBuildWidgetFor.toString()), key: ValueKey(dateToBuildWidgetFor.toString()),
date: dateToBuildWidgetFor.copyWith(isUtc: true), date: dateToBuildWidgetFor,
onDateSelected: (dateSelected) { onDateSelected: (dateSelected) {
if (dateSelected == null) return; if (dateSelected == null) return;

View File

@ -11,6 +11,7 @@ import 'package:calorimeter/utils/theme_switcher_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class PerDateWidget extends StatefulWidget { class PerDateWidget extends StatefulWidget {
final DateTime date; final DateTime date;
@ -25,13 +26,16 @@ class PerDateWidget extends StatefulWidget {
class _PerDateWidgetState extends State<PerDateWidget> class _PerDateWidgetState extends State<PerDateWidget>
with AutomaticKeepAliveClientMixin<PerDateWidget> { with AutomaticKeepAliveClientMixin<PerDateWidget> {
late FoodStorage storage; late FoodStorage storage;
late Future<List<FoodEntryState>> entriesFuture;
List<FoodEntryState> entries = []; List<FoodEntryState> entries = [];
@override @override
void initState() { void initState() {
context storage = FoodStorage.getInstance();
.read<FoodEntryBloc>() entriesFuture = storage.getEntriesForDate(widget.date);
.add(PageBeingInitialized(forDate: widget.date)); entriesFuture.then((val) {
entries = val;
});
super.initState(); super.initState();
} }
@ -44,37 +48,47 @@ class _PerDateWidgetState extends State<PerDateWidget>
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return BlocConsumer<FoodEntryBloc, GlobalEntryState>( return FutureBuilder(
future: entriesFuture,
builder: (context, snapshot) {
return snapshot.connectionState != ConnectionState.done
? const Center(child: CircularProgressIndicator())
: MultiProvider(
providers: [
BlocProvider(
create: (context) => FoodEntryBloc(
initialState: PageState(foodEntries: entries),
storage: storage,
forDate: widget.date,
),
)
],
child: BlocConsumer<FoodEntryBloc, PageState>(
listener: (context, pageState) { listener: (context, pageState) {
if (pageState.errorString != null) { if (pageState.errorString != null) {
showNewSnackbarWith(context, pageState.errorString!); showNewSnackbarWith(context, pageState.errorString!);
} }
}, },
builder: (context, globalState) { builder: (context, pageState) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(DateFormat.yMMMMd('de').format(widget.date)), title: Text(
DateFormat.yMMMMd('de').format(widget.date)),
actions: const [ThemeSwitcherButton()], actions: const [ThemeSwitcherButton()],
), ),
body: FoodEntryList( body: FoodEntryList(entries: pageState.foodEntries),
entries: globalState.foodEntries[widget.date] ?? [],
date: widget.date),
bottomNavigationBar: BottomAppBar( bottomNavigationBar: BottomAppBar(
shape: const RectangularNotchShape(), shape: const RectangularNotchShape(),
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
child: SumWidget( child: SumWidget(
foodEntries: globalState.foodEntries[widget.date] ?? [])), foodEntries: pageState.foodEntries)),
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: OverflowBar(children: [ floatingActionButton: OverflowBar(children: [
ScanFoodFloatingButton( ScanFoodFloatingButton(
onPressed: () { onPressed: () {
var result = BarcodeScanner.scan(); var result = BarcodeScanner.scan();
context.read<FoodEntryBloc>().add( context.read<FoodEntryBloc>().add(
BarcodeScanned( BarcodeScanned(scanResultFuture: result));
scanResultFuture: result,
forDate: widget.date,
),
);
}, },
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -88,7 +102,9 @@ class _PerDateWidgetState extends State<PerDateWidget>
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked); FloatingActionButtonLocation.endDocked);
}, },
),
); );
});
} }
void showNewSnackbarWith(BuildContext context, String text) { void showNewSnackbarWith(BuildContext context, String text) {