From 4481fdc8eb5b830e8d43a2228dd58beba49ea79e Mon Sep 17 00:00:00 2001 From: Simon Oppowa <24407484+simonoppowa@users.noreply.github.com> Date: Mon, 20 Jan 2025 20:49:40 +0100 Subject: [PATCH 1/4] feat: add daily kcal adjustment --- .../data/data_source/config_data_source.dart | 12 ++ lib/core/data/dbo/config_dbo.dart | 4 +- lib/core/data/dbo/config_dbo.g.dart | 7 +- .../data/repository/config_repository.dart | 8 + lib/core/domain/entity/config_entity.dart | 7 +- .../domain/usecase/add_config_usecase.dart | 4 + .../domain/usecase/get_kcal_goal_usecase.dart | 30 ++++ lib/core/utils/calc/calorie_goal_calc.dart | 4 +- lib/core/utils/locator.dart | 8 +- .../bloc/activity_detail_bloc.dart | 10 +- .../home/presentation/bloc/home_bloc.dart | 12 +- .../presentation/bloc/meal_detail_bloc.dart | 12 +- .../presentation/bloc/profile_bloc.dart | 17 +- .../presentation/bloc/settings_bloc.dart | 35 +++- .../widgets/calculations_dialog.dart | 161 ++++++++++++++++++ lib/features/settings/settings_screen.dart | 61 ++----- lib/generated/intl/messages_de.dart | 4 + lib/generated/intl/messages_en.dart | 3 + lib/generated/l10n.dart | 20 +++ lib/l10n/intl_de.arb | 27 +-- lib/l10n/intl_en.arb | 2 + pubspec.lock | 8 + 22 files changed, 343 insertions(+), 113 deletions(-) create mode 100644 lib/core/domain/usecase/get_kcal_goal_usecase.dart create mode 100644 lib/features/settings/presentation/widgets/calculations_dialog.dart diff --git a/lib/core/data/data_source/config_data_source.dart b/lib/core/data/data_source/config_data_source.dart index 9f2208ca..ec81b7c0 100644 --- a/lib/core/data/data_source/config_data_source.dart +++ b/lib/core/data/data_source/config_data_source.dart @@ -57,6 +57,18 @@ class ConfigDataSource { config?.save(); } + Future getKcalAdjustment() async { + final config = _configBox.get(_configKey); + return config?.kcalAdjustment ?? 0; + } + + Future setConfigKcalAdjustment(double kcalAdjustment) async { + _log.fine('Updating config kcalAdjustment to $kcalAdjustment'); + final config = _configBox.get(_configKey); + config?.kcalAdjustment = kcalAdjustment; + config?.save(); + } + Future getConfig() async { return _configBox.get(_configKey) ?? ConfigDBO.empty(); } diff --git a/lib/core/data/dbo/config_dbo.dart b/lib/core/data/dbo/config_dbo.dart index 96301d70..caa6b4d3 100644 --- a/lib/core/data/dbo/config_dbo.dart +++ b/lib/core/data/dbo/config_dbo.dart @@ -16,10 +16,12 @@ class ConfigDBO extends HiveObject { AppThemeDBO selectedAppTheme; @HiveField(4) bool? usesImperialUnits; + @HiveField(5) + double? kcalAdjustment; ConfigDBO(this.hasAcceptedDisclaimer, this.hasAcceptedPolicy, this.hasAcceptedSendAnonymousData, this.selectedAppTheme, - {this.usesImperialUnits = false}); + {this.usesImperialUnits = false, this.kcalAdjustment}); factory ConfigDBO.empty() => ConfigDBO(false, false, false, AppThemeDBO.system); diff --git a/lib/core/data/dbo/config_dbo.g.dart b/lib/core/data/dbo/config_dbo.g.dart index 6c1bea2b..849a57a8 100644 --- a/lib/core/data/dbo/config_dbo.g.dart +++ b/lib/core/data/dbo/config_dbo.g.dart @@ -22,13 +22,14 @@ class ConfigDBOAdapter extends TypeAdapter { fields[2] as bool, fields[3] as AppThemeDBO, usesImperialUnits: fields[4] as bool?, + kcalAdjustment: fields[5] as double?, ); } @override void write(BinaryWriter writer, ConfigDBO obj) { writer - ..writeByte(5) + ..writeByte(6) ..writeByte(0) ..write(obj.hasAcceptedDisclaimer) ..writeByte(1) @@ -38,7 +39,9 @@ class ConfigDBOAdapter extends TypeAdapter { ..writeByte(3) ..write(obj.selectedAppTheme) ..writeByte(4) - ..write(obj.usesImperialUnits); + ..write(obj.usesImperialUnits) + ..writeByte(5) + ..write(obj.kcalAdjustment); } @override diff --git a/lib/core/data/repository/config_repository.dart b/lib/core/data/repository/config_repository.dart index ee279a9b..d96c89b1 100644 --- a/lib/core/data/repository/config_repository.dart +++ b/lib/core/data/repository/config_repository.dart @@ -45,4 +45,12 @@ class ConfigRepository { Future setConfigUsesImperialUnits(bool usesImperialUnits) async { _configDataSource.setConfigUsesImperialUnits(usesImperialUnits); } + + Future getConfigKcalAdjustment() async { + return await _configDataSource.getKcalAdjustment(); + } + + Future setConfigKcalAdjustment(double kcalAdjustment) async { + _configDataSource.setConfigKcalAdjustment(kcalAdjustment); + } } diff --git a/lib/core/domain/entity/config_entity.dart b/lib/core/domain/entity/config_entity.dart index ec8ac52c..2cbba6bb 100644 --- a/lib/core/domain/entity/config_entity.dart +++ b/lib/core/domain/entity/config_entity.dart @@ -8,10 +8,11 @@ class ConfigEntity extends Equatable { final bool hasAcceptedSendAnonymousData; final AppThemeEntity appTheme; final bool usesImperialUnits; + final double? userKcalAdjustment; const ConfigEntity(this.hasAcceptedDisclaimer, this.hasAcceptedPolicy, this.hasAcceptedSendAnonymousData, this.appTheme, - {this.usesImperialUnits = false}); + {this.usesImperialUnits = false, this.userKcalAdjustment}); factory ConfigEntity.fromConfigDBO(ConfigDBO dbo) => ConfigEntity( dbo.hasAcceptedDisclaimer, @@ -19,6 +20,7 @@ class ConfigEntity extends Equatable { dbo.hasAcceptedSendAnonymousData, AppThemeEntity.fromAppThemeDBO(dbo.selectedAppTheme), usesImperialUnits: dbo.usesImperialUnits ?? false, + userKcalAdjustment: dbo.kcalAdjustment, ); @override @@ -26,6 +28,7 @@ class ConfigEntity extends Equatable { hasAcceptedDisclaimer, hasAcceptedPolicy, hasAcceptedSendAnonymousData, - usesImperialUnits + usesImperialUnits, + userKcalAdjustment, ]; } diff --git a/lib/core/domain/usecase/add_config_usecase.dart b/lib/core/domain/usecase/add_config_usecase.dart index 8fccb299..717ef750 100644 --- a/lib/core/domain/usecase/add_config_usecase.dart +++ b/lib/core/domain/usecase/add_config_usecase.dart @@ -28,4 +28,8 @@ class AddConfigUsecase { Future setConfigUsesImperialUnits(bool usesImperialUnits) async { _configRepository.setConfigUsesImperialUnits(usesImperialUnits); } + + Future setConfigKcalAdjustment(double kcalAdjustment) async { + _configRepository.setConfigKcalAdjustment(kcalAdjustment); + } } diff --git a/lib/core/domain/usecase/get_kcal_goal_usecase.dart b/lib/core/domain/usecase/get_kcal_goal_usecase.dart new file mode 100644 index 00000000..413da17f --- /dev/null +++ b/lib/core/domain/usecase/get_kcal_goal_usecase.dart @@ -0,0 +1,30 @@ +import 'package:collection/collection.dart'; +import 'package:opennutritracker/core/data/repository/config_repository.dart'; +import 'package:opennutritracker/core/data/repository/user_activity_repository.dart'; +import 'package:opennutritracker/core/data/repository/user_repository.dart'; +import 'package:opennutritracker/core/domain/entity/user_entity.dart'; +import 'package:opennutritracker/core/utils/calc/calorie_goal_calc.dart'; + +class GetKcalGoalUsecase { + final UserRepository userRepository; + final ConfigRepository configRepository; + final UserActivityRepository _userActivityRepository; + + GetKcalGoalUsecase( + this.userRepository, this.configRepository, this._userActivityRepository); + + Future getKcalGoal( + {UserEntity? userEntity, + double? totalKcalActivitiesParam, + double? kcalUserAdjustment}) async { + final user = userEntity ?? await userRepository.getUserData(); + final config = await configRepository.getConfig(); + final totalKcalActivities = totalKcalActivitiesParam ?? + (await _userActivityRepository.getAllUserActivityByDate(DateTime.now())) + .map((activity) => activity.burnedKcal) + .toList() + .sum; + return CalorieGoalCalc.getTotalKcalGoal(user, totalKcalActivities, + kcalUserAdjustment: config.userKcalAdjustment); + } +} diff --git a/lib/core/utils/calc/calorie_goal_calc.dart b/lib/core/utils/calc/calorie_goal_calc.dart index 4b9e228b..5e5d792b 100644 --- a/lib/core/utils/calc/calorie_goal_calc.dart +++ b/lib/core/utils/calc/calorie_goal_calc.dart @@ -15,9 +15,11 @@ class CalorieGoalCalc { TDEECalc.getTDEEKcalIOM2005(userEntity); static double getTotalKcalGoal( - UserEntity userEntity, double totalKcalActivities) => + UserEntity userEntity, double totalKcalActivities, + {double? kcalUserAdjustment}) => getTdee(userEntity) + getKcalGoalAdjustment(userEntity.goal) + + (kcalUserAdjustment ?? 0) + totalKcalActivities; static double getKcalGoalAdjustment(UserWeightGoalEntity goal) { diff --git a/lib/core/utils/locator.dart b/lib/core/utils/locator.dart index a1337469..4cea9458 100644 --- a/lib/core/utils/locator.dart +++ b/lib/core/utils/locator.dart @@ -21,6 +21,7 @@ import 'package:opennutritracker/core/domain/usecase/delete_intake_usecase.dart' import 'package:opennutritracker/core/domain/usecase/delete_user_activity_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_intake_usecase.dart'; +import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_physical_activity_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_tracked_day_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_user_activity_usecase.dart'; @@ -90,13 +91,14 @@ Future initLocator() async { locator(), locator(), locator(), locator(), locator(), locator())); locator.registerLazySingleton( () => ProfileBloc(locator(), locator(), locator(), locator(), locator())); - locator.registerLazySingleton(() => SettingsBloc(locator(), locator())); + locator.registerLazySingleton( + () => SettingsBloc(locator(), locator(), locator(), locator())); locator.registerFactory(() => ActivitiesBloc(locator())); locator.registerFactory( () => RecentActivitiesBloc(locator())); locator.registerFactory( - () => ActivityDetailBloc(locator(), locator(), locator())); + () => ActivityDetailBloc(locator(), locator(), locator(), locator())); locator.registerFactory( () => MealDetailBloc(locator(), locator(), locator())); locator.registerFactory(() => ScannerBloc(locator(), locator())); @@ -140,6 +142,8 @@ Future initLocator() async { () => GetTrackedDayUsecase(locator())); locator.registerLazySingleton( () => AddTrackedDayUsecase(locator())); + locator.registerLazySingleton( + () => GetKcalGoalUsecase(locator(), locator(), locator())); // Repositories locator.registerLazySingleton(() => ConfigRepository(locator())); diff --git a/lib/features/activity_detail/presentation/bloc/activity_detail_bloc.dart b/lib/features/activity_detail/presentation/bloc/activity_detail_bloc.dart index 355a3bab..81f0331b 100644 --- a/lib/features/activity_detail/presentation/bloc/activity_detail_bloc.dart +++ b/lib/features/activity_detail/presentation/bloc/activity_detail_bloc.dart @@ -6,8 +6,8 @@ import 'package:opennutritracker/core/domain/entity/user_activity_entity.dart'; import 'package:opennutritracker/core/domain/entity/user_entity.dart'; import 'package:opennutritracker/core/domain/usecase/add_tracked_day_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/add_user_activity_usercase.dart'; +import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_user_usecase.dart'; -import 'package:opennutritracker/core/utils/calc/calorie_goal_calc.dart'; import 'package:opennutritracker/core/utils/calc/macro_calc.dart'; import 'package:opennutritracker/core/utils/calc/met_calc.dart'; import 'package:opennutritracker/core/utils/id_generator.dart'; @@ -21,9 +21,10 @@ class ActivityDetailBloc final GetUserUsecase _getUserUsecase; final AddUserActivityUsecase _addUserActivityUsecase; final AddTrackedDayUsecase _addTrackedDayUsecase; + final GetKcalGoalUsecase _getKcalGoalUsecase; ActivityDetailBloc(this._getUserUsecase, this._addUserActivityUsecase, - this._addTrackedDayUsecase) + this._addTrackedDayUsecase, this._getKcalGoalUsecase) : super(ActivityDetailInitial()) { on((event, emit) async { emit(ActivityDetailLoadingState()); @@ -58,9 +59,8 @@ class ActivityDetailBloc } void _updateTrackedDay(DateTime dateTime, double caloriesBurned) async { - final userEntity = await _getUserUsecase.getUserData(); - final totalKcalGoal = - CalorieGoalCalc.getTotalKcalGoal(userEntity, caloriesBurned); + final totalKcalGoal = await _getKcalGoalUsecase.getKcalGoal( + totalKcalActivitiesParam: caloriesBurned); final totalCarbsGoal = MacroCalc.getTotalCarbsGoal(totalKcalGoal); final totalFatGoal = MacroCalc.getTotalFatsGoal(totalKcalGoal); final totalProteinGoal = MacroCalc.getTotalProteinsGoal(totalKcalGoal); diff --git a/lib/features/home/presentation/bloc/home_bloc.dart b/lib/features/home/presentation/bloc/home_bloc.dart index f1109ce8..9aad9cb3 100644 --- a/lib/features/home/presentation/bloc/home_bloc.dart +++ b/lib/features/home/presentation/bloc/home_bloc.dart @@ -9,8 +9,8 @@ import 'package:opennutritracker/core/domain/usecase/delete_intake_usecase.dart' import 'package:opennutritracker/core/domain/usecase/delete_user_activity_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_intake_usecase.dart'; +import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_user_activity_usecase.dart'; -import 'package:opennutritracker/core/domain/usecase/get_user_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/update_intake_usecase.dart'; import 'package:opennutritracker/core/utils/calc/calorie_goal_calc.dart'; import 'package:opennutritracker/core/utils/calc/macro_calc.dart'; @@ -30,8 +30,8 @@ class HomeBloc extends Bloc { final UpdateIntakeUsecase _updateIntakeUsecase; final GetUserActivityUsecase _getUserActivityUsecase; final DeleteUserActivityUsecase _deleteUserActivityUsecase; - final GetUserUsecase _getUserUsecase; final AddTrackedDayUsecase _addTrackedDayUseCase; + final GetKcalGoalUsecase _getKcalGoalUsecase; DateTime currentDay = DateTime.now(); @@ -43,8 +43,8 @@ class HomeBloc extends Bloc { this._updateIntakeUsecase, this._getUserActivityUsecase, this._deleteUserActivityUsecase, - this._getUserUsecase, - this._addTrackedDayUseCase) + this._addTrackedDayUseCase, + this._getKcalGoalUsecase) : super(HomeInitial()) { on((event, emit) async { emit(HomeLoadingState()); @@ -101,9 +101,7 @@ class HomeBloc extends Bloc { final totalKcalActivities = userActivities.map((activity) => activity.burnedKcal).toList().sum; - final user = await _getUserUsecase.getUserData(); - final totalKcalGoal = - CalorieGoalCalc.getTotalKcalGoal(user, totalKcalActivities); + final totalKcalGoal = await _getKcalGoalUsecase.getKcalGoal(); final totalCarbsGoal = MacroCalc.getTotalCarbsGoal(totalKcalGoal); final totalFatsGoal = MacroCalc.getTotalFatsGoal(totalKcalGoal); final totalProteinsGoal = MacroCalc.getTotalProteinsGoal(totalKcalGoal); diff --git a/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart b/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart index 611a49d6..3a74b81a 100644 --- a/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart +++ b/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart @@ -6,8 +6,7 @@ import 'package:opennutritracker/core/domain/entity/intake_entity.dart'; import 'package:opennutritracker/core/domain/entity/intake_type_entity.dart'; import 'package:opennutritracker/core/domain/usecase/add_intake_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/add_tracked_day_usecase.dart'; -import 'package:opennutritracker/core/domain/usecase/get_user_usecase.dart'; -import 'package:opennutritracker/core/utils/calc/calorie_goal_calc.dart'; +import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; import 'package:opennutritracker/core/utils/calc/macro_calc.dart'; import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; import 'package:opennutritracker/core/utils/id_generator.dart'; @@ -22,10 +21,10 @@ class MealDetailBloc extends Bloc { final log = Logger('MealDetailBloc'); final AddIntakeUsecase _addIntakeUseCase; final AddTrackedDayUsecase _addTrackedDayUsecase; - final GetUserUsecase _getUserUsecase; + final GetKcalGoalUsecase _getKcalGoalUsecase; - MealDetailBloc( - this._addIntakeUseCase, this._addTrackedDayUsecase, this._getUserUsecase) + MealDetailBloc(this._addIntakeUseCase, this._addTrackedDayUsecase, + this._getKcalGoalUsecase) : super(MealDetailInitial( totalQuantityConverted: '100', selectedUnit: UnitDropdownItem.gml.toString())) { @@ -88,8 +87,7 @@ class MealDetailBloc extends Bloc { IntakeEntity intakeEntity, DateTime day) async { final hasTrackedDay = await _addTrackedDayUsecase.hasTrackedDay(day); if (!hasTrackedDay) { - final userEntity = await _getUserUsecase.getUserData(); - final totalKcalGoal = CalorieGoalCalc.getTotalKcalGoal(userEntity, 0); + final totalKcalGoal = await _getKcalGoalUsecase.getKcalGoal(); final totalCarbsGoal = MacroCalc.getTotalCarbsGoal(totalKcalGoal); final totalFatGoal = MacroCalc.getTotalFatsGoal(totalKcalGoal); final totalProteinGoal = MacroCalc.getTotalProteinsGoal(totalKcalGoal); diff --git a/lib/features/profile/presentation/bloc/profile_bloc.dart b/lib/features/profile/presentation/bloc/profile_bloc.dart index 512caf75..500eeb84 100644 --- a/lib/features/profile/presentation/bloc/profile_bloc.dart +++ b/lib/features/profile/presentation/bloc/profile_bloc.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:opennutritracker/core/domain/entity/user_bmi_entity.dart'; @@ -6,10 +5,9 @@ import 'package:opennutritracker/core/domain/entity/user_entity.dart'; import 'package:opennutritracker/core/domain/usecase/add_tracked_day_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/add_user_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; -import 'package:opennutritracker/core/domain/usecase/get_user_activity_usecase.dart'; +import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_user_usecase.dart'; import 'package:opennutritracker/core/utils/calc/bmi_calc.dart'; -import 'package:opennutritracker/core/utils/calc/calorie_goal_calc.dart'; import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; import 'package:opennutritracker/core/utils/locator.dart'; import 'package:opennutritracker/features/diary/presentation/bloc/calendar_day_bloc.dart'; @@ -24,16 +22,15 @@ class ProfileBloc extends Bloc { final GetUserUsecase _getUserUsecase; final AddUserUsecase _addUserUsecase; final AddTrackedDayUsecase _addTrackedDayUsecase; - final GetUserActivityUsecase _getUserActivityUsecase; - final GetConfigUsecase _getConfigUsecase; + final GetKcalGoalUsecase _getKcalGoalUsecase; ProfileBloc( this._getUserUsecase, this._addUserUsecase, this._addTrackedDayUsecase, - this._getUserActivityUsecase, - this._getConfigUsecase) + this._getConfigUsecase, + this._getKcalGoalUsecase) : super(ProfileInitial()) { on((event, emit) async { emit(ProfileLoadingState()); @@ -72,12 +69,8 @@ class ProfileBloc extends Bloc { UserEntity user, DateTime day) async { final hasTrackedDay = await _addTrackedDayUsecase.hasTrackedDay(day); if (hasTrackedDay) { - final activityDayList = - await _getUserActivityUsecase.getTodayUserActivity(); - final totalActivityKcal = - activityDayList.map((activity) => activity.burnedKcal).sum; final totalKcalGoal = - CalorieGoalCalc.getTotalKcalGoal(user, totalActivityKcal); + await _getKcalGoalUsecase.getKcalGoal(userEntity: user); await _addTrackedDayUsecase.updateDayCalorieGoal(day, totalKcalGoal); } diff --git a/lib/features/settings/presentation/bloc/settings_bloc.dart b/lib/features/settings/presentation/bloc/settings_bloc.dart index b5a40b5e..53e546d6 100644 --- a/lib/features/settings/presentation/bloc/settings_bloc.dart +++ b/lib/features/settings/presentation/bloc/settings_bloc.dart @@ -3,8 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logging/logging.dart'; import 'package:opennutritracker/core/domain/entity/app_theme_entity.dart'; import 'package:opennutritracker/core/domain/usecase/add_config_usecase.dart'; +import 'package:opennutritracker/core/domain/usecase/add_tracked_day_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; +import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; import 'package:opennutritracker/core/utils/app_const.dart'; +import 'package:opennutritracker/core/utils/calc/macro_calc.dart'; part 'settings_event.dart'; @@ -15,8 +18,11 @@ class SettingsBloc extends Bloc { final GetConfigUsecase _getConfigUsecase; final AddConfigUsecase _addConfigUsecase; + final AddTrackedDayUsecase _addTrackedDayUsecase; + final GetKcalGoalUsecase _getKcalGoalUsecase; - SettingsBloc(this._getConfigUsecase, this._addConfigUsecase) + SettingsBloc(this._getConfigUsecase, this._addConfigUsecase, + this._addTrackedDayUsecase, this._getKcalGoalUsecase) : super(SettingsInitial()) { on((event, emit) async { emit(SettingsLoadingState()); @@ -45,6 +51,33 @@ class SettingsBloc extends Bloc { void setUsesImperialUnits(bool usesImperialUnits) { _addConfigUsecase.setConfigUsesImperialUnits(usesImperialUnits); } + + Future getKcalAdjustment() async { + final config = await _getConfigUsecase.getConfig(); + return config.userKcalAdjustment ?? 0; + } + + void setKcalAdjustment(double kcalAdjustment) { + _addConfigUsecase.setConfigKcalAdjustment(kcalAdjustment); + } + + void updateTrackedDay(DateTime day) async { + final day = DateTime.now(); + final totalKcalGoal = await _getKcalGoalUsecase.getKcalGoal(); + final totalCarbsGoal = MacroCalc.getTotalCarbsGoal(totalKcalGoal); + final totalFatGoal = MacroCalc.getTotalFatsGoal(totalKcalGoal); + final totalProteinGoal = MacroCalc.getTotalProteinsGoal(totalKcalGoal); + + final hasTrackedDay = await _addTrackedDayUsecase.hasTrackedDay(day); + + if (hasTrackedDay) { + await _addTrackedDayUsecase.updateDayCalorieGoal(day, totalKcalGoal); + await _addTrackedDayUsecase.updateDayMacroGoals(day, + carbsGoal: totalCarbsGoal, + fatGoal: totalFatGoal, + proteinGoal: totalProteinGoal); + } + } } enum SystemDropDownType { metric, imperial } diff --git a/lib/features/settings/presentation/widgets/calculations_dialog.dart b/lib/features/settings/presentation/widgets/calculations_dialog.dart new file mode 100644 index 00000000..07fad808 --- /dev/null +++ b/lib/features/settings/presentation/widgets/calculations_dialog.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:opennutritracker/features/diary/presentation/bloc/calendar_day_bloc.dart'; +import 'package:opennutritracker/features/diary/presentation/bloc/diary_bloc.dart'; +import 'package:opennutritracker/features/home/presentation/bloc/home_bloc.dart'; +import 'package:opennutritracker/features/profile/presentation/bloc/profile_bloc.dart'; +import 'package:opennutritracker/features/settings/presentation/bloc/settings_bloc.dart'; +import 'package:opennutritracker/generated/l10n.dart'; + +class CalculationsDialog extends StatefulWidget { + final SettingsBloc settingsBloc; + final ProfileBloc profileBloc; + final HomeBloc homeBloc; + final DiaryBloc diaryBloc; + final CalendarDayBloc calendarDayBloc; + + const CalculationsDialog({ + super.key, + required this.settingsBloc, + required this.profileBloc, + required this.homeBloc, + required this.diaryBloc, + required this.calendarDayBloc, + }); + + @override + State createState() => _CalculationsDialogState(); +} + +class _CalculationsDialogState extends State { + static const double _maxKcalAdjustment = 1000; + static const double _minKcalAdjustment = -1000; + static const int _kcalDivisions = 200; + double selectedKcalAdjustment = 0; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _initializeKcalAdjustment(); + } + + void _initializeKcalAdjustment() async { + final adjustment = await widget.settingsBloc.getKcalAdjustment() * + 1.0; // Convert to double + setState(() { + selectedKcalAdjustment = adjustment; + }); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).settingsCalculationsLabel), + TextButton( + child: Text(S.of(context).buttonResetLabel), + onPressed: () { + setState(() { + selectedKcalAdjustment = 0; + }); + }, + ), + ], + ), + content: Wrap( + children: [ + DropdownButtonFormField( + isExpanded: true, + decoration: InputDecoration( + enabled: false, + filled: false, + labelText: S.of(context).calculationsTDEELabel, + ), + items: [ + DropdownMenuItem( + child: Text( + '${S.of(context).calculationsTDEEIOM2006Label} ${S.of(context).calculationsRecommendedLabel}', + overflow: TextOverflow.ellipsis, + )), + ], + onChanged: null), + DropdownButtonFormField( + isExpanded: true, + decoration: InputDecoration( + enabled: false, + filled: false, + labelText: S + .of(context) + .calculationsMacronutrientsDistributionLabel), + items: [ + DropdownMenuItem( + child: Text( + S + .of(context) + .calculationsMacrosDistribution("60", "25", "15"), + overflow: TextOverflow.ellipsis, + )) + ], + onChanged: null), + const SizedBox(height: 64), + Container( + alignment: Alignment.centerLeft, + child: Text( + '${S.of(context).dailyKcalAdjustmentLabel} ${!selectedKcalAdjustment.isNegative ? "+" : ""}${selectedKcalAdjustment.round()} ${S.of(context).kcalLabel}', + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + Align( + alignment: Alignment.centerLeft, + child: SizedBox( + width: 280, + child: Slider( + min: _minKcalAdjustment, + max: _maxKcalAdjustment, + divisions: _kcalDivisions, + value: selectedKcalAdjustment, + label: + '${selectedKcalAdjustment.round()} ${S.of(context).kcalLabel}', + onChanged: (value) { + setState(() { + selectedKcalAdjustment = value; + }); + }, + ), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(S.of(context).dialogCancelLabel)), + TextButton( + onPressed: () { + _saveCalculationSettings(); + }, + child: Text(S.of(context).dialogOKLabel)) + ], + ); + } + + void _saveCalculationSettings() { + // Save the calorie offset as full number + widget.settingsBloc + .setKcalAdjustment(selectedKcalAdjustment.toInt().toDouble()); + widget.settingsBloc.add(LoadSettingsEvent()); + // Update other blocs that need the new calorie value + widget.profileBloc.add(LoadProfileEvent()); + widget.homeBloc.add(LoadItemsEvent()); + + // Update tracked day entity + widget.settingsBloc.updateTrackedDay(DateTime.now()); + widget.diaryBloc.add(LoadDiaryYearEvent()); + widget.calendarDayBloc.add(LoadCalendarDayEvent(DateTime.now())); + + Navigator.of(context).pop(); + } +} diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index f72e8148..99171c97 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -7,6 +7,7 @@ import 'package:opennutritracker/core/utils/app_const.dart'; import 'package:opennutritracker/core/utils/locator.dart'; import 'package:opennutritracker/core/utils/theme_mode_provider.dart'; import 'package:opennutritracker/core/utils/url_const.dart'; +import 'package:opennutritracker/features/diary/presentation/bloc/calendar_day_bloc.dart'; import 'package:opennutritracker/features/diary/presentation/bloc/diary_bloc.dart'; import 'package:opennutritracker/features/home/presentation/bloc/home_bloc.dart'; import 'package:opennutritracker/features/profile/presentation/bloc/profile_bloc.dart'; @@ -16,6 +17,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:opennutritracker/features/settings/presentation/widgets/calculations_dialog.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -29,6 +31,7 @@ class _SettingsScreenState extends State { late ProfileBloc _profileBloc; late HomeBloc _homeBloc; late DiaryBloc _diaryBloc; + late CalendarDayBloc _calendarDayBloc; @override void initState() { @@ -36,6 +39,7 @@ class _SettingsScreenState extends State { _profileBloc = locator(); _homeBloc = locator(); _diaryBloc = locator(); + _calendarDayBloc = locator(); super.initState(); } @@ -160,54 +164,15 @@ class _SettingsScreenState extends State { void _showCalculationsDialog(BuildContext context) { showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(S.of(context).settingsCalculationsLabel), - content: Wrap( - children: [ - DropdownButtonFormField( - isExpanded: true, - decoration: InputDecoration( - enabled: false, - filled: false, - labelText: S.of(context).calculationsTDEELabel, - ), - items: [ - DropdownMenuItem( - child: Text( - '${S.of(context).calculationsTDEEIOM2006Label} ${S.of(context).calculationsRecommendedLabel}', - overflow: TextOverflow.ellipsis, - )), - ], - onChanged: null), - DropdownButtonFormField( - isExpanded: true, - decoration: InputDecoration( - enabled: false, - filled: false, - labelText: S - .of(context) - .calculationsMacronutrientsDistributionLabel), - items: [ - DropdownMenuItem( - child: Text( - S - .of(context) - .calculationsMacrosDistribution("60", "25", "15"), - overflow: TextOverflow.ellipsis, - )) - ], - onChanged: null) - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(S.of(context).dialogOKLabel)) - ], - )); + context: context, + builder: (context) => CalculationsDialog( + settingsBloc: _settingsBloc, + profileBloc: _profileBloc, + homeBloc: _homeBloc, + diaryBloc: _diaryBloc, + calendarDayBloc: _calendarDayBloc, + ), + ); } void _showThemeDialog(BuildContext context, AppThemeEntity currentAppTheme) { diff --git a/lib/generated/intl/messages_de.dart b/lib/generated/intl/messages_de.dart index a7a9936c..752651e1 100644 --- a/lib/generated/intl/messages_de.dart +++ b/lib/generated/intl/messages_de.dart @@ -65,6 +65,8 @@ class MessageLookup extends MessageLookupByLibrary { "breakfastLabel": MessageLookupByLibrary.simpleMessage("Frühstück"), "burnedLabel": MessageLookupByLibrary.simpleMessage("verbrannt"), "buttonNextLabel": MessageLookupByLibrary.simpleMessage("WEITER"), + "buttonResetLabel": + MessageLookupByLibrary.simpleMessage("Zurücksetzen"), "buttonSaveLabel": MessageLookupByLibrary.simpleMessage("Speichern"), "buttonStartLabel": MessageLookupByLibrary.simpleMessage("START"), "buttonYesLabel": MessageLookupByLibrary.simpleMessage("JA"), @@ -94,6 +96,8 @@ class MessageLookup extends MessageLookupByLibrary { "Möchten Sie einen benutzerdefinierte Mahlzeit erstellen?"), "createCustomDialogTitle": MessageLookupByLibrary.simpleMessage( "Benutzerdefinierte Mahlzeit erstellen?"), + "dailyKcalAdjustmentLabel": + MessageLookupByLibrary.simpleMessage("Tägliche kcal-Anpassung:"), "dataCollectionLabel": MessageLookupByLibrary.simpleMessage( "Unterstützen der Entwicklung durch Bereitstellung anonymer Nutzungsdaten"), "deleteTimeDialogContent": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 975cea66..98f441dc 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -66,6 +66,7 @@ class MessageLookup extends MessageLookupByLibrary { "breakfastLabel": MessageLookupByLibrary.simpleMessage("Breakfast"), "burnedLabel": MessageLookupByLibrary.simpleMessage("burned"), "buttonNextLabel": MessageLookupByLibrary.simpleMessage("NEXT"), + "buttonResetLabel": MessageLookupByLibrary.simpleMessage("Reset"), "buttonSaveLabel": MessageLookupByLibrary.simpleMessage("Save"), "buttonStartLabel": MessageLookupByLibrary.simpleMessage("START"), "buttonYesLabel": MessageLookupByLibrary.simpleMessage("YES"), @@ -94,6 +95,8 @@ class MessageLookup extends MessageLookupByLibrary { "Do you want create a custom meal item?"), "createCustomDialogTitle": MessageLookupByLibrary.simpleMessage("Create custom meal item?"), + "dailyKcalAdjustmentLabel": + MessageLookupByLibrary.simpleMessage("Daily kcal adjustment:"), "dataCollectionLabel": MessageLookupByLibrary.simpleMessage( "Support development by providing anonymous usage data"), "deleteTimeDialogContent": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 680407cc..b7050294 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -320,6 +320,16 @@ class S { ); } + /// `Reset` + String get buttonResetLabel { + return Intl.message( + 'Reset', + name: 'buttonResetLabel', + desc: '', + args: [], + ); + } + /// `Welcome to` String get onboardingWelcomeLabel { return Intl.message( @@ -811,6 +821,16 @@ class S { ); } + /// `Daily kcal adjustment:` + String get dailyKcalAdjustmentLabel { + return Intl.message( + 'Daily kcal adjustment:', + name: 'dailyKcalAdjustmentLabel', + desc: '', + args: [], + ); + } + /// `Add new Item:` String get addItemLabel { return Intl.message( diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 222f7a48..bf91023f 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -4,7 +4,6 @@ "appDescription": "OpenNutriTracker ist ein kostenloser und quelloffener Kalorien- und Nährstofftracker, der Ihre Privatsphäre respektiert.", "alphaVersionName": "[Alpha]", "betaVersionName": "[Beta]", - "addLabel": "Hinzufügen", "createCustomDialogTitle": "Benutzerdefinierte Mahlzeit erstellen?", "createCustomDialogContent": "Möchten Sie einen benutzerdefinierte Mahlzeit erstellen?", @@ -21,14 +20,13 @@ "recentlyAddedLabel": "Kürzlich", "noMealsRecentlyAddedLabel": "Keine kürzlich hinzugefügten Mahlzeiten", "noActivityRecentlyAddedLabel": "Keine kürzlich hinzugefügten Aktivitäten", - "dialogOKLabel": "OK", "dialogCancelLabel": "ABBRECHEN", "buttonStartLabel": "START", "buttonNextLabel": "WEITER", "buttonSaveLabel": "Speichern", "buttonYesLabel": "JA", - + "buttonResetLabel": "Zurücksetzen", "onboardingWelcomeLabel": "Willkommen bei", "onboardingOverviewLabel": "Übersicht", "onboardingYourGoalLabel": "Ihr Kalorienziel:", @@ -48,7 +46,6 @@ "onboardingActivityQuestionSubtitle": "Wie aktiv sind Sie? (Ohne Trainingseinheiten)", "onboardingGoalQuestionSubtitle": "Was ist Ihr aktuelles Gewichtsziel?", "onboardingSaveUserError": "Falsche Eingabe, bitte versuchen Sie es erneut", - "settingsUnitsLabel": "Einheiten", "settingsCalculationsLabel": "Berechnungen", "settingsThemeLabel": "Thema", @@ -74,7 +71,7 @@ "calculationsRecommendedLabel": "(empfohlen)", "calculationsMacronutrientsDistributionLabel": "Verteilung der Makronährstoffe", "calculationsMacrosDistribution": "{pctCarbs}% Kohlenhydrate, {pctFats}% Fette, {pctProteins}% Proteine", - + "dailyKcalAdjustmentLabel": "Tägliche kcal-Anpassung:", "addItemLabel": "Neuen Eintrag hinzufügen:", "activityLabel": "Aktivität", "activityExample": "z. B. Laufen, Radfahren, Yoga ...", @@ -86,25 +83,19 @@ "dinnerExample": "z. B. Suppe, Hähnchen, Wein ...", "snackLabel": "Snack", "snackExample": "z. B. Apfel, Eiscreme, Schokolade ...", - "editItemDialogTitle": "Eintrag aktualisieren", "itemUpdatedSnackbar": "Eintrag aktualisiert", - "deleteTimeDialogTitle": "Eintrag löschen?", "deleteTimeDialogContent": "Möchten Sie den ausgewählten Eintrag löschen?", "itemDeletedSnackbar": "Eintrag gelöscht", - "copyDialogTitle": "Zu welcher Mahlzeit hinzufügen?", - "copyOrDeleteTimeDialogTitle": "Was soll getan werden?", "copyOrDeleteTimeDialogContent": "Auf \"Nach heute kopieren\" klicken, um die Mahlzeit nach heute zu kopieren. Mit \"Löschen\" kann die Mahlzeit entfernt werden", "dialogCopyLabel": "NACH HEUTE KOPIEREN", "dialogDeleteLabel": "LÖSCHEN", - "suppliedLabel": "zugeführt", "burnedLabel": "verbrannt", "kcalLeftLabel": "kcal übrig", - "nutritionInfoLabel": "Nährwertangaben", "kcalLabel": "kcal", "carbsLabel": "Kohlenhydrate", @@ -129,10 +120,8 @@ "milliliterUnit": "ml", "gramMilliliterUnit": "g/ml", "missingProductInfo": "Produkt fehlen die erforderlichen Angaben zu Kalorien oder Makronährstoffen", - "infoAddedIntakeLabel": "Neue Aufnahme hinzugefügt", "infoAddedActivityLabel": "Neue Aktivität hinzugefügt", - "editMealLabel": "Mahlzeit bearbeiten", "mealNameLabel": "Mahlzeitenname", "mealBrandsLabel": "Marken", @@ -147,13 +136,11 @@ "mealFatLabel": "Fett pro 100 g/ml", "mealProteinLabel": "Protein pro 100 g/ml", "errorMealSave": "Fehler beim Speichern der Mahlzeit. Haben Sie die korrekten Mahlzeiteninformationen eingegeben?", - "bmiLabel": "BMI", "bmiInfo": "Der Body-Mass-Index (BMI) ist ein Index zur Klassifizierung von Übergewicht und Fettleibigkeit bei Erwachsenen. Er wird berechnet, indem das Gewicht in Kilogramm durch das Quadrat der Körpergröße in Metern (kg/m²) geteilt wird.\n\nDer BMI unterscheidet nicht zwischen Fett- und Muskelmasse und kann für einige Personen irreführend sein.", "readLabel": "Ich habe die Datenschutzbestimmungen gelesen und akzeptiere sie.", "privacyPolicyLabel": "Datenschutzrichtlinie", "dataCollectionLabel": "Unterstützen der Entwicklung durch Bereitstellung anonymer Nutzungsdaten", - "palSedentaryLabel": "Sitzend", "palSedentaryDescriptionLabel": "z. B. Büroarbeit und hauptsächlich sitzende Freizeitaktivitäten", "palLowLActiveLabel": "Leicht aktiv", @@ -162,7 +149,6 @@ "palActiveDescriptionLabel": "Überwiegend Stehen oder Gehen bei der Arbeit und aktive Freizeitaktivitäten", "palVeryActiveLabel": "Sehr aktiv", "palVeryActiveDescriptionLabel": "Überwiegend Gehen, Laufen oder Gewichte tragen bei der Arbeit und aktive Freizeitaktivitäten", - "selectPalCategoryLabel": "Aktivitätslevel auswählen", "chooseWeightGoalLabel": "Gewichtsziel wählen", "goalLoseWeight": "Gewicht verlieren", @@ -181,16 +167,13 @@ "genderLabel": "Geschlecht", "genderMaleLabel": "♂ männlich", "genderFemaleLabel": "♀ weiblich", - "nothingAddedLabel": "Nichts hinzugefügt", - "nutritionalStatusUnderweight": "Untergewicht", "nutritionalStatusNormalWeight": "Normales Gewicht", "nutritionalStatusPreObesity": "Prä-Adipositas", "nutritionalStatusObeseClassI": "Fettleibigkeit Klasse I", "nutritionalStatusObeseClassII": "Fettleibigkeit Klasse II", "nutritionalStatusObeseClassIII": "Fettleibigkeit Klasse III", - "nutritionalStatusRiskLabel": "Risiko für Begleiterkrankungen: {riskValue}", "nutritionalStatusRiskLow": "Niedrig \n(aber erhöhtes Risiko für andere \nklinische Probleme)", "nutritionalStatusRiskAverage": "Durchschnittlich", @@ -198,7 +181,6 @@ "nutritionalStatusRiskModerate": "Mäßig", "nutritionalStatusRiskSevere": "Schwerwiegend", "nutritionalStatusRiskVerySevere": "Sehr schwerwiegend", - "errorOpeningEmail": "Fehler beim Öffnen der E-Mail-Anwendung", "errorOpeningBrowser": "Fehler beim Öffnen der Browser-Anwendung", "errorFetchingProductData": "Fehler beim Abrufen von Produktinformationen", @@ -206,9 +188,7 @@ "errorLoadingActivities": "Fehler beim Laden von Aktivitäten", "noResultsFound": "Keine Ergebnisse gefunden", "retryLabel": "Erneut versuchen", - "@PHYSICAL_ACTIVITIES": {}, - "paHeadingBicycling": "Radfahren", "paHeadingConditionalExercise": "Konditionstraining", "paHeadingDancing": "Tanzen", @@ -217,9 +197,7 @@ "paHeadingWalking": "Gehen", "paHeadingWaterActivities": "Wassersport", "paHeadingWinterActivities": "Winteraktivitäten", - "paGeneralDesc": "allgemein", - "paBicyclingGeneral": "Radfahren", "paBicyclingGeneralDesc": "allgemein", "paBicyclingMountainGeneral": "Mountainbiking", @@ -409,5 +387,4 @@ "paSkiingGeneralDesc": "allgemein", "paSnowShovingModerate": "Schnee schaufeln", "paSnowShovingModerateDesc": "manuell, mäßige Anstrengung" - } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b43e5ce1..81af2fea 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -26,6 +26,7 @@ "buttonNextLabel": "NEXT", "buttonSaveLabel": "Save", "buttonYesLabel": "YES", + "buttonResetLabel": "Reset", "onboardingWelcomeLabel": "Welcome to", "onboardingOverviewLabel": "Overview", "onboardingYourGoalLabel": "Your calorie goal:", @@ -75,6 +76,7 @@ "calculationsRecommendedLabel": "(recommended)", "calculationsMacronutrientsDistributionLabel": "Macros distribution", "calculationsMacrosDistribution": "{pctCarbs}% carbs, {pctFats}% fats, {pctProteins}% proteins", + "dailyKcalAdjustmentLabel": "Daily kcal adjustment:", "addItemLabel": "Add new Item:", "activityLabel": "Activity", "activityExample": "e.g. running, biking, yoga ...", diff --git a/pubspec.lock b/pubspec.lock index 54d275e4..4b808a99 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -762,6 +762,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6 + url: "https://pub.dev" + source: hosted + version: "5.4.5" nested: dependency: transitive description: From 6b23f93445e9dd86a27429c61c98c18762090c34 Mon Sep 17 00:00:00 2001 From: Simon Oppowa <24407484+simonoppowa@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:41:16 +0100 Subject: [PATCH 2/4] feat: add user macro distribution settings --- .../data/data_source/config_data_source.dart | 25 +- lib/core/data/dbo/config_dbo.dart | 10 +- lib/core/data/dbo/config_dbo.g.dart | 17 +- .../data/repository/config_repository.dart | 6 + lib/core/domain/entity/config_entity.dart | 17 +- .../domain/usecase/add_config_usecase.dart | 5 + .../domain/usecase/get_kcal_goal_usecase.dart | 10 +- .../usecase/get_macro_goal_usecase.dart | 32 +++ lib/core/utils/calc/macro_calc.dart | 25 +- lib/core/utils/locator.dart | 13 +- .../bloc/activity_detail_bloc.dart | 18 +- .../home/presentation/bloc/home_bloc.dart | 14 +- .../presentation/bloc/meal_detail_bloc.dart | 14 +- .../presentation/bloc/settings_bloc.dart | 41 +++- .../widgets/calculations_dialog.dart | 214 +++++++++++++++--- lib/generated/intl/messages_de.dart | 2 + lib/generated/intl/messages_en.dart | 4 +- lib/generated/l10n.dart | 14 +- lib/l10n/intl_de.arb | 1 + lib/l10n/intl_en.arb | 3 +- 20 files changed, 405 insertions(+), 80 deletions(-) create mode 100644 lib/core/domain/usecase/get_macro_goal_usecase.dart diff --git a/lib/core/data/data_source/config_data_source.dart b/lib/core/data/data_source/config_data_source.dart index ec81b7c0..fc4a1651 100644 --- a/lib/core/data/data_source/config_data_source.dart +++ b/lib/core/data/data_source/config_data_source.dart @@ -59,13 +59,34 @@ class ConfigDataSource { Future getKcalAdjustment() async { final config = _configBox.get(_configKey); - return config?.kcalAdjustment ?? 0; + return config?.userKcalAdjustment ?? 0; } Future setConfigKcalAdjustment(double kcalAdjustment) async { _log.fine('Updating config kcalAdjustment to $kcalAdjustment'); final config = _configBox.get(_configKey); - config?.kcalAdjustment = kcalAdjustment; + config?.userKcalAdjustment = kcalAdjustment; + config?.save(); + } + + Future setConfigCarbGoalPct(double carbGoalPct) async { + _log.fine('Updating config carbGoalPct to $carbGoalPct'); + final config = _configBox.get(_configKey); + config?.userCarbGoalPct = carbGoalPct; + config?.save(); + } + + Future setConfigProteinGoalPct(double proteinGoalPct) async { + _log.fine('Updating config proteinGoalPct to $proteinGoalPct'); + final config = _configBox.get(_configKey); + config?.userProteinGoalPct = proteinGoalPct; + config?.save(); + } + + Future setConfigFatGoalPct(double fatGoalPct) async { + _log.fine('Updating config fatGoalPct to $fatGoalPct'); + final config = _configBox.get(_configKey); + config?.userFatGoalPct = fatGoalPct; config?.save(); } diff --git a/lib/core/data/dbo/config_dbo.dart b/lib/core/data/dbo/config_dbo.dart index caa6b4d3..455e7a8c 100644 --- a/lib/core/data/dbo/config_dbo.dart +++ b/lib/core/data/dbo/config_dbo.dart @@ -17,11 +17,17 @@ class ConfigDBO extends HiveObject { @HiveField(4) bool? usesImperialUnits; @HiveField(5) - double? kcalAdjustment; + double? userKcalAdjustment; + @HiveField(6) + double? userCarbGoalPct; + @HiveField(7) + double? userProteinGoalPct; + @HiveField(8) + double? userFatGoalPct; ConfigDBO(this.hasAcceptedDisclaimer, this.hasAcceptedPolicy, this.hasAcceptedSendAnonymousData, this.selectedAppTheme, - {this.usesImperialUnits = false, this.kcalAdjustment}); + {this.usesImperialUnits = false, this.userKcalAdjustment}); factory ConfigDBO.empty() => ConfigDBO(false, false, false, AppThemeDBO.system); diff --git a/lib/core/data/dbo/config_dbo.g.dart b/lib/core/data/dbo/config_dbo.g.dart index 849a57a8..f015ac0a 100644 --- a/lib/core/data/dbo/config_dbo.g.dart +++ b/lib/core/data/dbo/config_dbo.g.dart @@ -22,14 +22,17 @@ class ConfigDBOAdapter extends TypeAdapter { fields[2] as bool, fields[3] as AppThemeDBO, usesImperialUnits: fields[4] as bool?, - kcalAdjustment: fields[5] as double?, - ); + userKcalAdjustment: fields[5] as double?, + ) + ..userCarbGoalPct = fields[6] as double? + ..userProteinGoalPct = fields[7] as double? + ..userFatGoalPct = fields[8] as double?; } @override void write(BinaryWriter writer, ConfigDBO obj) { writer - ..writeByte(6) + ..writeByte(9) ..writeByte(0) ..write(obj.hasAcceptedDisclaimer) ..writeByte(1) @@ -41,7 +44,13 @@ class ConfigDBOAdapter extends TypeAdapter { ..writeByte(4) ..write(obj.usesImperialUnits) ..writeByte(5) - ..write(obj.kcalAdjustment); + ..write(obj.userKcalAdjustment) + ..writeByte(6) + ..write(obj.userCarbGoalPct) + ..writeByte(7) + ..write(obj.userProteinGoalPct) + ..writeByte(8) + ..write(obj.userFatGoalPct); } @override diff --git a/lib/core/data/repository/config_repository.dart b/lib/core/data/repository/config_repository.dart index d96c89b1..d7fc9683 100644 --- a/lib/core/data/repository/config_repository.dart +++ b/lib/core/data/repository/config_repository.dart @@ -53,4 +53,10 @@ class ConfigRepository { Future setConfigKcalAdjustment(double kcalAdjustment) async { _configDataSource.setConfigKcalAdjustment(kcalAdjustment); } + + Future setUserMacroPct(double carbs, double protein, double fat) async { + _configDataSource.setConfigCarbGoalPct(carbs); + _configDataSource.setConfigProteinGoalPct(protein); + _configDataSource.setConfigFatGoalPct(fat); + } } diff --git a/lib/core/domain/entity/config_entity.dart b/lib/core/domain/entity/config_entity.dart index 2cbba6bb..7edb21a7 100644 --- a/lib/core/domain/entity/config_entity.dart +++ b/lib/core/domain/entity/config_entity.dart @@ -9,10 +9,17 @@ class ConfigEntity extends Equatable { final AppThemeEntity appTheme; final bool usesImperialUnits; final double? userKcalAdjustment; + final double? userCarbGoalPct; + final double? userProteinGoalPct; + final double? userFatGoalPct; const ConfigEntity(this.hasAcceptedDisclaimer, this.hasAcceptedPolicy, this.hasAcceptedSendAnonymousData, this.appTheme, - {this.usesImperialUnits = false, this.userKcalAdjustment}); + {this.usesImperialUnits = false, + this.userKcalAdjustment, + this.userCarbGoalPct, + this.userProteinGoalPct, + this.userFatGoalPct}); factory ConfigEntity.fromConfigDBO(ConfigDBO dbo) => ConfigEntity( dbo.hasAcceptedDisclaimer, @@ -20,7 +27,10 @@ class ConfigEntity extends Equatable { dbo.hasAcceptedSendAnonymousData, AppThemeEntity.fromAppThemeDBO(dbo.selectedAppTheme), usesImperialUnits: dbo.usesImperialUnits ?? false, - userKcalAdjustment: dbo.kcalAdjustment, + userKcalAdjustment: dbo.userKcalAdjustment, + userCarbGoalPct: dbo.userCarbGoalPct, + userProteinGoalPct: dbo.userProteinGoalPct, + userFatGoalPct: dbo.userFatGoalPct, ); @override @@ -30,5 +40,8 @@ class ConfigEntity extends Equatable { hasAcceptedSendAnonymousData, usesImperialUnits, userKcalAdjustment, + userCarbGoalPct, + userProteinGoalPct, + userFatGoalPct, ]; } diff --git a/lib/core/domain/usecase/add_config_usecase.dart b/lib/core/domain/usecase/add_config_usecase.dart index 717ef750..f6742b3c 100644 --- a/lib/core/domain/usecase/add_config_usecase.dart +++ b/lib/core/domain/usecase/add_config_usecase.dart @@ -32,4 +32,9 @@ class AddConfigUsecase { Future setConfigKcalAdjustment(double kcalAdjustment) async { _configRepository.setConfigKcalAdjustment(kcalAdjustment); } + + Future setConfigMacroGoalPct( + double carbGoalPct, double proteinGoalPct, double fatPctGoal) async { + _configRepository.setUserMacroPct(carbGoalPct, proteinGoalPct, fatPctGoal); + } } diff --git a/lib/core/domain/usecase/get_kcal_goal_usecase.dart b/lib/core/domain/usecase/get_kcal_goal_usecase.dart index 413da17f..df076ad1 100644 --- a/lib/core/domain/usecase/get_kcal_goal_usecase.dart +++ b/lib/core/domain/usecase/get_kcal_goal_usecase.dart @@ -6,19 +6,19 @@ import 'package:opennutritracker/core/domain/entity/user_entity.dart'; import 'package:opennutritracker/core/utils/calc/calorie_goal_calc.dart'; class GetKcalGoalUsecase { - final UserRepository userRepository; - final ConfigRepository configRepository; + final UserRepository _userRepository; + final ConfigRepository _configRepository; final UserActivityRepository _userActivityRepository; GetKcalGoalUsecase( - this.userRepository, this.configRepository, this._userActivityRepository); + this._userRepository, this._configRepository, this._userActivityRepository); Future getKcalGoal( {UserEntity? userEntity, double? totalKcalActivitiesParam, double? kcalUserAdjustment}) async { - final user = userEntity ?? await userRepository.getUserData(); - final config = await configRepository.getConfig(); + final user = userEntity ?? await _userRepository.getUserData(); + final config = await _configRepository.getConfig(); final totalKcalActivities = totalKcalActivitiesParam ?? (await _userActivityRepository.getAllUserActivityByDate(DateTime.now())) .map((activity) => activity.burnedKcal) diff --git a/lib/core/domain/usecase/get_macro_goal_usecase.dart b/lib/core/domain/usecase/get_macro_goal_usecase.dart new file mode 100644 index 00000000..a72fc54f --- /dev/null +++ b/lib/core/domain/usecase/get_macro_goal_usecase.dart @@ -0,0 +1,32 @@ +import 'package:opennutritracker/core/data/repository/config_repository.dart'; +import 'package:opennutritracker/core/utils/calc/macro_calc.dart'; + +class GetMacroGoalUsecase { + final ConfigRepository _configRepository; + + GetMacroGoalUsecase(this._configRepository); + + Future getCarbsGoal(double totalCalorieGoal) async { + final config = await _configRepository.getConfig(); + final userCarbGoal = config.userCarbGoalPct; + + return MacroCalc.getTotalCarbsGoal(totalCalorieGoal, + userCarbsGoal: userCarbGoal); + } + + Future getFatsGoal(double totalCalorieGoal) async { + final config = await _configRepository.getConfig(); + final userFatGoal = config.userFatGoalPct; + + return MacroCalc.getTotalFatsGoal(totalCalorieGoal, + userFatsGoal: userFatGoal); + } + + Future getProteinsGoal(double totalCalorieGoal) async { + final config = await _configRepository.getConfig(); + final userProteinGoal = config.userProteinGoalPct; + + return MacroCalc.getTotalProteinsGoal(totalCalorieGoal, + userProteinsGoal: userProteinGoal); + } +} diff --git a/lib/core/utils/calc/macro_calc.dart b/lib/core/utils/calc/macro_calc.dart index 888e2ab6..9d11197b 100644 --- a/lib/core/utils/calc/macro_calc.dart +++ b/lib/core/utils/calc/macro_calc.dart @@ -12,12 +12,25 @@ class MacroCalc { static const _defaultFatsPercentageGoal = 0.25; static const _defaultProteinsPercentageGoal = 0.15; - static double getTotalCarbsGoal(double totalCalorieGoal) => - (totalCalorieGoal * _defaultCarbsPercentageGoal) / _carbsKcalPerGram; + /// Calculate the total carbs goal based on the total calorie goal + /// Uses the default percentage if the user has not set a goal + static double getTotalCarbsGoal( + double totalCalorieGoal, {double? userCarbsGoal}) => + (totalCalorieGoal * (userCarbsGoal ?? _defaultCarbsPercentageGoal)) / + _carbsKcalPerGram; - static double getTotalFatsGoal(double totalCalorieGoal) => - (totalCalorieGoal * _defaultFatsPercentageGoal) / _fatKcalPerGram; + /// Calculate the total fats goal based on the total calorie goal + /// Uses the default percentage if the user has not set a goal + static double getTotalFatsGoal( + double totalCalorieGoal, {double? userFatsGoal}) => + (totalCalorieGoal * (userFatsGoal ?? _defaultFatsPercentageGoal)) / + _fatKcalPerGram; - static double getTotalProteinsGoal(double totalCalorieGoal) => - (totalCalorieGoal * _defaultProteinsPercentageGoal) / _proteinKcalPerGram; + /// Calculate the total proteins goal based on the total calorie goal + /// Uses the default percentage if the user has not set a goal + static double getTotalProteinsGoal( + double totalCalorieGoal, {double? userProteinsGoal}) => + (totalCalorieGoal * + (userProteinsGoal ?? _defaultProteinsPercentageGoal)) / + _proteinKcalPerGram; } diff --git a/lib/core/utils/locator.dart b/lib/core/utils/locator.dart index 4cea9458..d0fb119c 100644 --- a/lib/core/utils/locator.dart +++ b/lib/core/utils/locator.dart @@ -22,6 +22,7 @@ import 'package:opennutritracker/core/domain/usecase/delete_user_activity_usecas import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_intake_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; +import 'package:opennutritracker/core/domain/usecase/get_macro_goal_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_physical_activity_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_tracked_day_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_user_activity_usecase.dart'; @@ -85,22 +86,23 @@ Future initLocator() async { locator(), locator(), locator(), + locator(), locator())); locator.registerLazySingleton(() => DiaryBloc(locator(), locator())); locator.registerLazySingleton(() => CalendarDayBloc( locator(), locator(), locator(), locator(), locator(), locator())); locator.registerLazySingleton( () => ProfileBloc(locator(), locator(), locator(), locator(), locator())); - locator.registerLazySingleton( - () => SettingsBloc(locator(), locator(), locator(), locator())); + locator.registerLazySingleton(() => + SettingsBloc(locator(), locator(), locator(), locator(), locator())); locator.registerFactory(() => ActivitiesBloc(locator())); locator.registerFactory( () => RecentActivitiesBloc(locator())); - locator.registerFactory( - () => ActivityDetailBloc(locator(), locator(), locator(), locator())); + locator.registerFactory(() => ActivityDetailBloc( + locator(), locator(), locator(), locator(), locator())); locator.registerFactory( - () => MealDetailBloc(locator(), locator(), locator())); + () => MealDetailBloc(locator(), locator(), locator(), locator())); locator.registerFactory(() => ScannerBloc(locator(), locator())); locator.registerFactory(() => EditMealBloc(locator())); locator.registerFactory(() => AddMealBloc(locator())); @@ -144,6 +146,7 @@ Future initLocator() async { () => AddTrackedDayUsecase(locator())); locator.registerLazySingleton( () => GetKcalGoalUsecase(locator(), locator(), locator())); + locator.registerLazySingleton(() => GetMacroGoalUsecase(locator())); // Repositories locator.registerLazySingleton(() => ConfigRepository(locator())); diff --git a/lib/features/activity_detail/presentation/bloc/activity_detail_bloc.dart b/lib/features/activity_detail/presentation/bloc/activity_detail_bloc.dart index 81f0331b..a4b05871 100644 --- a/lib/features/activity_detail/presentation/bloc/activity_detail_bloc.dart +++ b/lib/features/activity_detail/presentation/bloc/activity_detail_bloc.dart @@ -7,6 +7,7 @@ import 'package:opennutritracker/core/domain/entity/user_entity.dart'; import 'package:opennutritracker/core/domain/usecase/add_tracked_day_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/add_user_activity_usercase.dart'; import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; +import 'package:opennutritracker/core/domain/usecase/get_macro_goal_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_user_usecase.dart'; import 'package:opennutritracker/core/utils/calc/macro_calc.dart'; import 'package:opennutritracker/core/utils/calc/met_calc.dart'; @@ -22,9 +23,14 @@ class ActivityDetailBloc final AddUserActivityUsecase _addUserActivityUsecase; final AddTrackedDayUsecase _addTrackedDayUsecase; final GetKcalGoalUsecase _getKcalGoalUsecase; + final GetMacroGoalUsecase _getMacroGoalUsecase; - ActivityDetailBloc(this._getUserUsecase, this._addUserActivityUsecase, - this._addTrackedDayUsecase, this._getKcalGoalUsecase) + ActivityDetailBloc( + this._getUserUsecase, + this._addUserActivityUsecase, + this._addTrackedDayUsecase, + this._getKcalGoalUsecase, + this._getMacroGoalUsecase) : super(ActivityDetailInitial()) { on((event, emit) async { emit(ActivityDetailLoadingState()); @@ -61,9 +67,11 @@ class ActivityDetailBloc void _updateTrackedDay(DateTime dateTime, double caloriesBurned) async { final totalKcalGoal = await _getKcalGoalUsecase.getKcalGoal( totalKcalActivitiesParam: caloriesBurned); - final totalCarbsGoal = MacroCalc.getTotalCarbsGoal(totalKcalGoal); - final totalFatGoal = MacroCalc.getTotalFatsGoal(totalKcalGoal); - final totalProteinGoal = MacroCalc.getTotalProteinsGoal(totalKcalGoal); + final totalCarbsGoal = + await _getMacroGoalUsecase.getCarbsGoal(totalKcalGoal); + final totalFatGoal = await _getMacroGoalUsecase.getFatsGoal(totalKcalGoal); + final totalProteinGoal = + await _getMacroGoalUsecase.getProteinsGoal(totalKcalGoal); final hasTrackedDay = await _addTrackedDayUsecase.hasTrackedDay(DateTime.now()); diff --git a/lib/features/home/presentation/bloc/home_bloc.dart b/lib/features/home/presentation/bloc/home_bloc.dart index 9aad9cb3..ef7e7ab1 100644 --- a/lib/features/home/presentation/bloc/home_bloc.dart +++ b/lib/features/home/presentation/bloc/home_bloc.dart @@ -10,6 +10,7 @@ import 'package:opennutritracker/core/domain/usecase/delete_user_activity_usecas import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_intake_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; +import 'package:opennutritracker/core/domain/usecase/get_macro_goal_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_user_activity_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/update_intake_usecase.dart'; import 'package:opennutritracker/core/utils/calc/calorie_goal_calc.dart'; @@ -32,6 +33,7 @@ class HomeBloc extends Bloc { final DeleteUserActivityUsecase _deleteUserActivityUsecase; final AddTrackedDayUsecase _addTrackedDayUseCase; final GetKcalGoalUsecase _getKcalGoalUsecase; + final GetMacroGoalUsecase _getMacroGoalUsecase; DateTime currentDay = DateTime.now(); @@ -44,7 +46,8 @@ class HomeBloc extends Bloc { this._getUserActivityUsecase, this._deleteUserActivityUsecase, this._addTrackedDayUseCase, - this._getKcalGoalUsecase) + this._getKcalGoalUsecase, + this._getMacroGoalUsecase) : super(HomeInitial()) { on((event, emit) async { emit(HomeLoadingState()); @@ -102,9 +105,12 @@ class HomeBloc extends Bloc { userActivities.map((activity) => activity.burnedKcal).toList().sum; final totalKcalGoal = await _getKcalGoalUsecase.getKcalGoal(); - final totalCarbsGoal = MacroCalc.getTotalCarbsGoal(totalKcalGoal); - final totalFatsGoal = MacroCalc.getTotalFatsGoal(totalKcalGoal); - final totalProteinsGoal = MacroCalc.getTotalProteinsGoal(totalKcalGoal); + final totalCarbsGoal = + await _getMacroGoalUsecase.getCarbsGoal(totalKcalGoal); + final totalFatsGoal = + await _getMacroGoalUsecase.getFatsGoal(totalKcalGoal); + final totalProteinsGoal = + await _getMacroGoalUsecase.getProteinsGoal(totalKcalGoal); final totalKcalLeft = CalorieGoalCalc.getDailyKcalLeft(totalKcalGoal, totalKcalIntake); diff --git a/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart b/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart index 3a74b81a..0de16afe 100644 --- a/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart +++ b/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart @@ -7,7 +7,7 @@ import 'package:opennutritracker/core/domain/entity/intake_type_entity.dart'; import 'package:opennutritracker/core/domain/usecase/add_intake_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/add_tracked_day_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; -import 'package:opennutritracker/core/utils/calc/macro_calc.dart'; +import 'package:opennutritracker/core/domain/usecase/get_macro_goal_usecase.dart'; import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; import 'package:opennutritracker/core/utils/id_generator.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; @@ -22,9 +22,10 @@ class MealDetailBloc extends Bloc { final AddIntakeUsecase _addIntakeUseCase; final AddTrackedDayUsecase _addTrackedDayUsecase; final GetKcalGoalUsecase _getKcalGoalUsecase; + final GetMacroGoalUsecase _getMacroGoalUsecase; MealDetailBloc(this._addIntakeUseCase, this._addTrackedDayUsecase, - this._getKcalGoalUsecase) + this._getKcalGoalUsecase, this._getMacroGoalUsecase) : super(MealDetailInitial( totalQuantityConverted: '100', selectedUnit: UnitDropdownItem.gml.toString())) { @@ -88,9 +89,12 @@ class MealDetailBloc extends Bloc { final hasTrackedDay = await _addTrackedDayUsecase.hasTrackedDay(day); if (!hasTrackedDay) { final totalKcalGoal = await _getKcalGoalUsecase.getKcalGoal(); - final totalCarbsGoal = MacroCalc.getTotalCarbsGoal(totalKcalGoal); - final totalFatGoal = MacroCalc.getTotalFatsGoal(totalKcalGoal); - final totalProteinGoal = MacroCalc.getTotalProteinsGoal(totalKcalGoal); + final totalCarbsGoal = + await _getMacroGoalUsecase.getCarbsGoal(totalKcalGoal); + final totalFatGoal = + await _getMacroGoalUsecase.getFatsGoal(totalKcalGoal); + final totalProteinGoal = + await _getMacroGoalUsecase.getProteinsGoal(totalKcalGoal); await _addTrackedDayUsecase.addNewTrackedDay( day, totalKcalGoal, totalCarbsGoal, totalFatGoal, totalProteinGoal); diff --git a/lib/features/settings/presentation/bloc/settings_bloc.dart b/lib/features/settings/presentation/bloc/settings_bloc.dart index 53e546d6..5b47c874 100644 --- a/lib/features/settings/presentation/bloc/settings_bloc.dart +++ b/lib/features/settings/presentation/bloc/settings_bloc.dart @@ -6,8 +6,8 @@ import 'package:opennutritracker/core/domain/usecase/add_config_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/add_tracked_day_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_kcal_goal_usecase.dart'; +import 'package:opennutritracker/core/domain/usecase/get_macro_goal_usecase.dart'; import 'package:opennutritracker/core/utils/app_const.dart'; -import 'package:opennutritracker/core/utils/calc/macro_calc.dart'; part 'settings_event.dart'; @@ -20,9 +20,14 @@ class SettingsBloc extends Bloc { final AddConfigUsecase _addConfigUsecase; final AddTrackedDayUsecase _addTrackedDayUsecase; final GetKcalGoalUsecase _getKcalGoalUsecase; - - SettingsBloc(this._getConfigUsecase, this._addConfigUsecase, - this._addTrackedDayUsecase, this._getKcalGoalUsecase) + final GetMacroGoalUsecase _getMacroGoalUsecase; + + SettingsBloc( + this._getConfigUsecase, + this._addConfigUsecase, + this._addTrackedDayUsecase, + this._getKcalGoalUsecase, + this._getMacroGoalUsecase) : super(SettingsInitial()) { on((event, emit) async { emit(SettingsLoadingState()); @@ -57,16 +62,38 @@ class SettingsBloc extends Bloc { return config.userKcalAdjustment ?? 0; } + Future getUserCarbGoalPct() async { + final config = await _getConfigUsecase.getConfig(); + return config.userCarbGoalPct; + } + + Future getUserProteinGoalPct() async { + final config = await _getConfigUsecase.getConfig(); + return config.userProteinGoalPct; + } + + Future getUserFatGoalPct() async { + final config = await _getConfigUsecase.getConfig(); + return config.userFatGoalPct; + } + void setKcalAdjustment(double kcalAdjustment) { _addConfigUsecase.setConfigKcalAdjustment(kcalAdjustment); } + void setMacroGoals( + double carbGoalPct, double proteinGoalPct, double fatGoalPct) { + _addConfigUsecase.setConfigMacroGoalPct(carbGoalPct.toInt() / 100, + proteinGoalPct.toInt() / 100, fatGoalPct.toInt() / 100); + } void updateTrackedDay(DateTime day) async { final day = DateTime.now(); final totalKcalGoal = await _getKcalGoalUsecase.getKcalGoal(); - final totalCarbsGoal = MacroCalc.getTotalCarbsGoal(totalKcalGoal); - final totalFatGoal = MacroCalc.getTotalFatsGoal(totalKcalGoal); - final totalProteinGoal = MacroCalc.getTotalProteinsGoal(totalKcalGoal); + final totalCarbsGoal = + await _getMacroGoalUsecase.getCarbsGoal(totalKcalGoal); + final totalFatGoal = await _getMacroGoalUsecase.getFatsGoal(totalKcalGoal); + final totalProteinGoal = + await _getMacroGoalUsecase.getProteinsGoal(totalKcalGoal); final hasTrackedDay = await _addTrackedDayUsecase.hasTrackedDay(day); diff --git a/lib/features/settings/presentation/widgets/calculations_dialog.dart b/lib/features/settings/presentation/widgets/calculations_dialog.dart index 07fad808..6288222c 100644 --- a/lib/features/settings/presentation/widgets/calculations_dialog.dart +++ b/lib/features/settings/presentation/widgets/calculations_dialog.dart @@ -30,7 +30,16 @@ class _CalculationsDialogState extends State { static const double _maxKcalAdjustment = 1000; static const double _minKcalAdjustment = -1000; static const int _kcalDivisions = 200; - double selectedKcalAdjustment = 0; + double _kcalAdjustmentSelection = 0; + + static const double _defaultCarbsPctSelection = 0.6; + static const double _defaultFatPctSelection = 0.25; + static const double _defaultProteinPctSelection = 0.15; + + // Macros percentages + double _carbsPctSelection = _defaultCarbsPctSelection * 100; + double _proteinPctSelection = _defaultProteinPctSelection * 100; + double _fatPctSelection = _defaultFatPctSelection * 100; @override void didChangeDependencies() { @@ -39,10 +48,18 @@ class _CalculationsDialogState extends State { } void _initializeKcalAdjustment() async { - final adjustment = await widget.settingsBloc.getKcalAdjustment() * + final kcalAdjustment = await widget.settingsBloc.getKcalAdjustment() * 1.0; // Convert to double + final userCarbsPct = await widget.settingsBloc.getUserCarbGoalPct(); + final userProteinPct = await widget.settingsBloc.getUserProteinGoalPct(); + final userFatPct = await widget.settingsBloc.getUserFatGoalPct(); + setState(() { - selectedKcalAdjustment = adjustment; + _kcalAdjustmentSelection = kcalAdjustment; + _carbsPctSelection = (userCarbsPct ?? _defaultCarbsPctSelection) * 100; + _proteinPctSelection = + (userProteinPct ?? _defaultProteinPctSelection) * 100; + _fatPctSelection = (userFatPct ?? _defaultFatPctSelection) * 100; }); } @@ -52,12 +69,22 @@ class _CalculationsDialogState extends State { title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(S.of(context).settingsCalculationsLabel), + Expanded( + child: Text( + S.of(context).settingsCalculationsLabel, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), // Add spacing between text and button TextButton( child: Text(S.of(context).buttonResetLabel), onPressed: () { setState(() { - selectedKcalAdjustment = 0; + _kcalAdjustmentSelection = 0; + // Reset macros to default values + _carbsPctSelection = _defaultCarbsPctSelection * 100; + _proteinPctSelection = _defaultProteinPctSelection * 100; + _fatPctSelection = _defaultFatPctSelection * 100; }); }, ), @@ -80,30 +107,12 @@ class _CalculationsDialogState extends State { )), ], onChanged: null), - DropdownButtonFormField( - isExpanded: true, - decoration: InputDecoration( - enabled: false, - filled: false, - labelText: S - .of(context) - .calculationsMacronutrientsDistributionLabel), - items: [ - DropdownMenuItem( - child: Text( - S - .of(context) - .calculationsMacrosDistribution("60", "25", "15"), - overflow: TextOverflow.ellipsis, - )) - ], - onChanged: null), const SizedBox(height: 64), Container( alignment: Alignment.centerLeft, child: Text( - '${S.of(context).dailyKcalAdjustmentLabel} ${!selectedKcalAdjustment.isNegative ? "+" : ""}${selectedKcalAdjustment.round()} ${S.of(context).kcalLabel}', - style: Theme.of(context).textTheme.bodyMedium, + '${S.of(context).dailyKcalAdjustmentLabel} ${!_kcalAdjustmentSelection.isNegative ? "+" : ""}${_kcalAdjustmentSelection.round()} ${S.of(context).kcalLabel}', + style: Theme.of(context).textTheme.titleMedium, ), ), Align( @@ -114,17 +123,115 @@ class _CalculationsDialogState extends State { min: _minKcalAdjustment, max: _maxKcalAdjustment, divisions: _kcalDivisions, - value: selectedKcalAdjustment, + value: _kcalAdjustmentSelection, label: - '${selectedKcalAdjustment.round()} ${S.of(context).kcalLabel}', + '${_kcalAdjustmentSelection.round()} ${S.of(context).kcalLabel}', onChanged: (value) { setState(() { - selectedKcalAdjustment = value; + _kcalAdjustmentSelection = value; }); }, ), ), ), + const SizedBox(height: 32), + Text( + S.of(context).macroDistributionLabel, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 32), + _buildMacroSlider( + S.of(context).carbsLabel, + _carbsPctSelection, + Colors.orange, + (value) { + setState(() { + double delta = value - _carbsPctSelection; + _carbsPctSelection = value; + + // Adjust other percentages proportionally + double proteinRatio = _proteinPctSelection / + (_proteinPctSelection + _fatPctSelection); + double fatRatio = _fatPctSelection / + (_proteinPctSelection + _fatPctSelection); + + _proteinPctSelection -= delta * proteinRatio; + _fatPctSelection -= delta * fatRatio; + + // Ensure no value goes below 5% + if (_proteinPctSelection < 5) { + double overflow = 5 - _proteinPctSelection; + _proteinPctSelection = 5; + _fatPctSelection -= overflow; + } + if (_fatPctSelection < 5) { + double overflow = 5 - _fatPctSelection; + _fatPctSelection = 5; + _proteinPctSelection -= overflow; + } + }); + }, + ), + _buildMacroSlider( + S.of(context).proteinLabel, + _proteinPctSelection, + Colors.blue, + (value) { + setState(() { + double delta = value - _proteinPctSelection; + _proteinPctSelection = value; + + double carbsRatio = _carbsPctSelection / + (_carbsPctSelection + _fatPctSelection); + double fatRatio = + _fatPctSelection / (_carbsPctSelection + _fatPctSelection); + + _carbsPctSelection -= delta * carbsRatio; + _fatPctSelection -= delta * fatRatio; + + if (_carbsPctSelection < 5) { + double overflow = 5 - _carbsPctSelection; + _carbsPctSelection = 5; + _fatPctSelection -= overflow; + } + if (_fatPctSelection < 5) { + double overflow = 5 - _fatPctSelection; + _fatPctSelection = 5; + _carbsPctSelection -= overflow; + } + }); + }, + ), + _buildMacroSlider( + S.of(context).fatLabel, + _fatPctSelection, + Colors.green, + (value) { + setState(() { + double delta = value - _fatPctSelection; + _fatPctSelection = value; + + double carbsRatio = _carbsPctSelection / + (_carbsPctSelection + _proteinPctSelection); + double proteinRatio = _proteinPctSelection / + (_carbsPctSelection + _proteinPctSelection); + + _carbsPctSelection -= delta * carbsRatio; + _proteinPctSelection -= delta * proteinRatio; + + if (_carbsPctSelection < 5) { + double overflow = 5 - _carbsPctSelection; + _carbsPctSelection = 5; + _proteinPctSelection -= overflow; + } + if (_proteinPctSelection < 5) { + double overflow = 5 - _proteinPctSelection; + _proteinPctSelection = 5; + _carbsPctSelection -= overflow; + } + }); + }, + ), ], ), actions: [ @@ -142,10 +249,59 @@ class _CalculationsDialogState extends State { ); } + Widget _buildMacroSlider( + String label, + double value, + Color color, + ValueChanged onChanged, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label), + Text('${value.round()}%'), + ], + ), + SizedBox( + width: 280, + child: SliderTheme( + data: SliderThemeData( + activeTrackColor: color, + thumbColor: color, + inactiveTrackColor: color.withValues(alpha: 0.2), + ), + child: Slider( + min: 5, + // Minimum 5% + max: 90, + // Maximum 90% + value: value, + divisions: 85, + onChanged: (newValue) { + // Only allow changes if the total will remain 100% + double otherValues = 100 - newValue; + if (otherValues >= 10) { + // Ensure at least 5% each for other macros + onChanged(newValue); + } + }, + ), + ), + ), + ], + ); + } + void _saveCalculationSettings() { // Save the calorie offset as full number widget.settingsBloc - .setKcalAdjustment(selectedKcalAdjustment.toInt().toDouble()); + .setKcalAdjustment(_kcalAdjustmentSelection.toInt().toDouble()); + widget.settingsBloc.setMacroGoals( + _carbsPctSelection, _proteinPctSelection, _fatPctSelection); + widget.settingsBloc.add(LoadSettingsEvent()); // Update other blocs that need the new calorie value widget.profileBloc.add(LoadProfileEvent()); diff --git a/lib/generated/intl/messages_de.dart b/lib/generated/intl/messages_de.dart index 752651e1..19016575 100644 --- a/lib/generated/intl/messages_de.dart +++ b/lib/generated/intl/messages_de.dart @@ -162,6 +162,8 @@ class MessageLookup extends MessageLookupByLibrary { "lunchExample": MessageLookupByLibrary.simpleMessage( "z. B. Pizza, Salat, Reis ..."), "lunchLabel": MessageLookupByLibrary.simpleMessage("Mittagessen"), + "macroDistributionLabel": + MessageLookupByLibrary.simpleMessage("Makronährstoff-Verteilung:"), "mealBrandsLabel": MessageLookupByLibrary.simpleMessage("Marken"), "mealCarbsLabel": MessageLookupByLibrary.simpleMessage("Kohlenhydrate pro 100 g/ml"), diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 98f441dc..7a2287a3 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -96,7 +96,7 @@ class MessageLookup extends MessageLookupByLibrary { "createCustomDialogTitle": MessageLookupByLibrary.simpleMessage("Create custom meal item?"), "dailyKcalAdjustmentLabel": - MessageLookupByLibrary.simpleMessage("Daily kcal adjustment:"), + MessageLookupByLibrary.simpleMessage("Daily Kcal adjustment:"), "dataCollectionLabel": MessageLookupByLibrary.simpleMessage( "Support development by providing anonymous usage data"), "deleteTimeDialogContent": MessageLookupByLibrary.simpleMessage( @@ -161,6 +161,8 @@ class MessageLookup extends MessageLookupByLibrary { "lunchExample": MessageLookupByLibrary.simpleMessage("e.g. pizza, salad, rice ..."), "lunchLabel": MessageLookupByLibrary.simpleMessage("Lunch"), + "macroDistributionLabel": + MessageLookupByLibrary.simpleMessage("Macronutrient Distribution:"), "mealBrandsLabel": MessageLookupByLibrary.simpleMessage("Brands"), "mealCarbsLabel": MessageLookupByLibrary.simpleMessage("carbs per"), "mealFatLabel": MessageLookupByLibrary.simpleMessage("fat per"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index b7050294..9a764b23 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -821,16 +821,26 @@ class S { ); } - /// `Daily kcal adjustment:` + /// `Daily Kcal adjustment:` String get dailyKcalAdjustmentLabel { return Intl.message( - 'Daily kcal adjustment:', + 'Daily Kcal adjustment:', name: 'dailyKcalAdjustmentLabel', desc: '', args: [], ); } + /// `Macronutrient Distribution:` + String get macroDistributionLabel { + return Intl.message( + 'Macronutrient Distribution:', + name: 'macroDistributionLabel', + desc: '', + args: [], + ); + } + /// `Add new Item:` String get addItemLabel { return Intl.message( diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index bf91023f..740dabac 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -72,6 +72,7 @@ "calculationsMacronutrientsDistributionLabel": "Verteilung der Makronährstoffe", "calculationsMacrosDistribution": "{pctCarbs}% Kohlenhydrate, {pctFats}% Fette, {pctProteins}% Proteine", "dailyKcalAdjustmentLabel": "Tägliche kcal-Anpassung:", + "macroDistributionLabel": "Makronährstoff-Verteilung:", "addItemLabel": "Neuen Eintrag hinzufügen:", "activityLabel": "Aktivität", "activityExample": "z. B. Laufen, Radfahren, Yoga ...", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 81af2fea..0f320396 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -76,7 +76,8 @@ "calculationsRecommendedLabel": "(recommended)", "calculationsMacronutrientsDistributionLabel": "Macros distribution", "calculationsMacrosDistribution": "{pctCarbs}% carbs, {pctFats}% fats, {pctProteins}% proteins", - "dailyKcalAdjustmentLabel": "Daily kcal adjustment:", + "dailyKcalAdjustmentLabel": "Daily Kcal adjustment:", + "macroDistributionLabel": "Macronutrient Distribution:", "addItemLabel": "Add new Item:", "activityLabel": "Activity", "activityExample": "e.g. running, biking, yoga ...", From ac9d58374c7c2674f36989909e22cd5c5f7b23d0 Mon Sep 17 00:00:00 2001 From: Simon Oppowa <24407484+simonoppowa@users.noreply.github.com> Date: Sat, 25 Jan 2025 17:52:27 +0100 Subject: [PATCH 3/4] feat: add normalize macros --- .../widgets/calculations_dialog.dart | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/lib/features/settings/presentation/widgets/calculations_dialog.dart b/lib/features/settings/presentation/widgets/calculations_dialog.dart index 6288222c..6886deaf 100644 --- a/lib/features/settings/presentation/widgets/calculations_dialog.dart +++ b/lib/features/settings/presentation/widgets/calculations_dialog.dart @@ -275,17 +275,14 @@ class _CalculationsDialogState extends State { ), child: Slider( min: 5, - // Minimum 5% max: 90, - // Maximum 90% value: value, divisions: 85, - onChanged: (newValue) { - // Only allow changes if the total will remain 100% - double otherValues = 100 - newValue; - if (otherValues >= 10) { - // Ensure at least 5% each for other macros + onChanged: (value) { + final newValue = value.round().toDouble(); + if (100 - newValue >= 10) { onChanged(newValue); + _normalizeMacros(); } }, ), @@ -295,6 +292,48 @@ class _CalculationsDialogState extends State { ); } + void _normalizeMacros() { + setState(() { + // First, ensure all values are rounded + _carbsPctSelection = _carbsPctSelection.roundToDouble(); + _proteinPctSelection = _proteinPctSelection.roundToDouble(); + _fatPctSelection = _fatPctSelection.roundToDouble(); + + // Calculate total + double total = + _carbsPctSelection + _proteinPctSelection + _fatPctSelection; + + // If total isn't 100, adjust values proportionally + if (total != 100) { + // Calculate adjustment factor + double factor = 100 / total; + + // Adjust the first two values + _carbsPctSelection = (_carbsPctSelection * factor).roundToDouble(); + _proteinPctSelection = (_proteinPctSelection * factor).roundToDouble(); + + // Set the last value to make total exactly 100 + _fatPctSelection = 100 - _carbsPctSelection - _proteinPctSelection; + + // Ensure minimum values (5%) + if (_fatPctSelection < 5) { + _fatPctSelection = 5; + // Distribute remaining 95% proportionally between carbs and protein + double remaining = 95; + double ratio = + _carbsPctSelection / (_carbsPctSelection + _proteinPctSelection); + _carbsPctSelection = (remaining * ratio).roundToDouble(); + _proteinPctSelection = remaining - _carbsPctSelection; + } + } + + // Verify final values + assert( + _carbsPctSelection + _proteinPctSelection + _fatPctSelection == 100, + 'Macros must total 100%'); + }); + } + void _saveCalculationSettings() { // Save the calorie offset as full number widget.settingsBloc From 35a194b568d4598d140305c7239515d11a43edea Mon Sep 17 00:00:00 2001 From: Simon Oppowa <24407484+simonoppowa@users.noreply.github.com> Date: Sun, 26 Jan 2025 17:29:04 +0100 Subject: [PATCH 4/4] v0.8.0+34 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index a5842e0e..2db0772c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.7.2+31 +version: 0.8.0+34 environment: sdk: '>=3.0.0 <4.0.0'