From c5f5a798078b9cc487d8a2fabfe4f9c40784aff1 Mon Sep 17 00:00:00 2001
From: Turtlepaw <81275769+Turtlepaw@users.noreply.github.com>
Date: Wed, 30 Oct 2024 01:26:49 -0400
Subject: [PATCH] feat: add water (hydration), distance, and calories
---
android/app/src/main/AndroidManifest.xml | 3 +
lib/constants.dart | 8 +-
lib/main.dart | 40 ++-
lib/routes/challenge.dart | 2 +-
lib/routes/challenges/bingo.dart | 42 +++-
lib/routes/create.dart | 6 +-
lib/routes/settings.dart | 305 +++++++++++++----------
lib/utils/bingo/data.dart | 24 +-
lib/utils/bingo/manager.dart | 103 ++++++--
lib/utils/common.dart | 6 +-
lib/utils/health.dart | 75 +++++-
pubspec.lock | 9 +
pubspec.yaml | 3 +
13 files changed, 440 insertions(+), 186 deletions(-)
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 1c8c968..22e65c0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -10,6 +10,9 @@
+
+
+
diff --git a/lib/constants.dart b/lib/constants.dart
index 877ecf7..9a77fe8 100644
--- a/lib/constants.dart
+++ b/lib/constants.dart
@@ -5,11 +5,11 @@ const String apiUrl = "https://fitnesschallenges.webredirect.org";
const types = [
HealthDataType.STEPS,
HealthDataType.HEART_RATE,
+ HealthDataType.DISTANCE_DELTA,
+ HealthDataType.WATER,
+ HealthDataType.TOTAL_CALORIES_BURNED,
];
-const permissions = [
- HealthDataAccess.READ,
- HealthDataAccess.READ,
-];
+var permissions = types.map((type) => HealthDataAccess.READ).toList();
final pbDateFormat = DateFormat('yyyy-MM-dd HH:mm:ss');
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
index 8638ad1..0b1ceef 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -173,7 +173,7 @@ void callbackDispatcher() {
final pb = await initializePocketbase();
if (!pb.authStore.isValid) return Future.value(false);
final type = await HealthTypeManager().getHealthType();
- if(type == null) {
+ if (type == null) {
logger.debug("Health type not set, canceling work");
return Future.value(false);
}
@@ -318,7 +318,7 @@ void main() async {
manager.init();
healthManager.checkConnectionState();
Future.delayed(const Duration(seconds: 1), () {
- healthManager.fetchHealthData();
+ healthManager.fetchHealthData();
});
Health().configure(useHealthConnectIfAvailable: true);
final wearManager = WearManager(pb).sendAuthentication(logger);
@@ -554,6 +554,7 @@ class _HomePageState extends State {
final challengeProvider =
Provider.of(context, listen: true);
final mediaQuery = MediaQuery.of(context);
+ final theme = Theme.of(context);
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
@@ -592,13 +593,42 @@ class _HomePageState extends State {
);
})
else
- const Center(
+ Center(
+ child: Container(
+ padding:
+ const EdgeInsets.symmetric(vertical: 25, horizontal: 30),
+ decoration: BoxDecoration(
+ color: theme.colorScheme.surfaceContainerLow,
+ borderRadius: BorderRadius.circular(15),
+ border: Border.all(
+ color: theme.colorScheme.surfaceContainerHigh,
+ width: 1.1,
+ style: BorderStyle.solid,
+ strokeAlign: BorderSide.strokeAlignCenter,
+ )),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
- children: [Text("No challenges...")],
+ children: [
+ Icon(
+ Symbols.travel_explore_rounded,
+ size: 50,
+ color: theme.colorScheme.primary,
+ ),
+ const SizedBox(height: 15),
+ Text("You aren't in any challenges yet.",
+ style: theme.textTheme.titleLarge),
+ Text("Create or join a challenge to get started.",
+ style: theme.textTheme.bodyLarge),
+ const SizedBox(height: 5),
+ FilledButton.tonalIcon(
+ onPressed: () => _showBottomSheet(context),
+ icon: const Icon(Symbols.stylus_note_rounded),
+ label: const Text("Create or join"),
+ )
+ ],
),
- ),
+ )),
const SizedBox(height: 25)
],
),
diff --git a/lib/routes/challenge.dart b/lib/routes/challenge.dart
index 141e0a8..b7d4fe1 100644
--- a/lib/routes/challenge.dart
+++ b/lib/routes/challenge.dart
@@ -643,7 +643,7 @@ class _ChallengeDialogState extends State {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
- "${formatNumber(data['totalValue'] as int)} steps",
+ "${formatInt(data['totalValue'] as int)} steps",
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onPrimary),
),
diff --git a/lib/routes/challenges/bingo.dart b/lib/routes/challenges/bingo.dart
index eac8666..df308b1 100644
--- a/lib/routes/challenges/bingo.dart
+++ b/lib/routes/challenges/bingo.dart
@@ -2,6 +2,7 @@ import 'dart:math';
import 'package:confetti/confetti.dart'; // Import the confetti package
import 'package:dynamic_color/dynamic_color.dart';
+import 'package:fitness_challenges/utils/health.dart';
import 'package:flutter/material.dart';
import 'package:flutter_advanced_avatar/flutter_advanced_avatar.dart';
import 'package:material_symbols_icons/symbols.dart';
@@ -9,6 +10,7 @@ import 'package:pocketbase/pocketbase.dart';
import 'package:provider/provider.dart';
import '../../utils/bingo/data.dart';
+import '../../utils/common.dart';
class BingoCardWidget extends StatefulWidget {
final Widget Function(BuildContext) buildTopDetails;
@@ -111,6 +113,7 @@ class _BingoCardWidgetState extends State {
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
+ final health = Provider.of(context);
return LayoutBuilder(
builder: (context, constraints) {
@@ -233,7 +236,7 @@ class _BingoCardWidgetState extends State {
),
const SizedBox(height: 10),
Text(
- activity.amount.toString(),
+ formatNumber(activity.amount),
textAlign: TextAlign.center,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onPrimary,
@@ -270,6 +273,25 @@ class _BingoCardWidgetState extends State {
),
),
+ Padding(
+ padding: const EdgeInsets.only(left: 8),
+ child: GridView(
+ physics: const NeverScrollableScrollPhysics(),
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 5,
+ crossAxisSpacing: 5.0,
+ mainAxisSpacing: 5.0,
+ ),
+ shrinkWrap: true,
+ children: [
+ _buildDataBlock(
+ health.steps
+ ),
+
+ ],
+ ),
+ ),
+
// Horizontal scroll of other users' bingo cards
_buildOtherUsersCards(manager),
@@ -281,6 +303,24 @@ class _BingoCardWidgetState extends State {
);
}
+ Widget _buildDataBlock(int? value){
+ var theme = Theme.of(context);
+
+ return Row(
+ children: [
+ const Icon(
+ Symbols.steps_rounded,
+ size: 15,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ formatNumber(value ?? 0),
+ style: theme.textTheme.bodyLarge,
+ )
+ ],
+ );
+ }
+
Widget _buildOtherUsersCards(BingoDataManager manager) {
var theme = Theme.of(context);
diff --git a/lib/routes/create.dart b/lib/routes/create.dart
index 351037c..009d38b 100644
--- a/lib/routes/create.dart
+++ b/lib/routes/create.dart
@@ -75,12 +75,12 @@ class _CreateDialogState extends State {
ReactiveFormConsumer(builder: (context, form, widget) =>
_isCreating
? const Padding(
- padding: EdgeInsets.symmetric(horizontal: 15),
+ padding: EdgeInsets.only(right: 25),
child: SizedBox(
width: 15,
height: 15,
child: CircularProgressIndicator(
- strokeCap: StrokeCap.round),
+ strokeCap: StrokeCap.round, strokeWidth: 3.2,),
)
)
: TextButton(
@@ -287,7 +287,7 @@ class CreateWidget extends StatelessWidget {
.control(type)
.value == index,
);
- }).toList(),
+ }),
],
),
// const SizedBox(
diff --git a/lib/routes/settings.dart b/lib/routes/settings.dart
index 98b8d72..3e77a8a 100644
--- a/lib/routes/settings.dart
+++ b/lib/routes/settings.dart
@@ -1,6 +1,7 @@
import 'package:fitness_challenges/components/loader.dart';
import 'package:fitness_challenges/constants.dart';
import 'package:fitness_challenges/routes/profile.dart';
+import 'package:fitness_challenges/utils/bingo/data.dart';
import 'package:fitness_challenges/utils/common.dart';
import 'package:fitness_challenges/utils/health.dart';
import 'package:flutter/material.dart';
@@ -72,8 +73,8 @@ class _SettingsPageState extends State {
}
Future _connectSystemHealthPlatform(bool _) async {
- final result = await Health().requestAuthorization(types,
- permissions: [HealthDataAccess.READ, HealthDataAccess.READ]);
+ final result =
+ await Health().requestAuthorization(types, permissions: permissions);
if (result == true) {
if (mounted) {
@@ -81,16 +82,17 @@ class _SettingsPageState extends State {
await HealthTypeManager().setHealthType(HealthType.systemManaged);
setState(() {
- healthType = HealthType.systemManaged;
+ //healthType = HealthType.systemManaged;
_isRefreshing = true;
});
Future.delayed(const Duration(seconds: 1));
- await Provider.of(context, listen: false)
+ var result = await Provider.of(context, listen: false)
.fetchHealthData(context: context);
setState(() {
+ if (result == true) healthType = HealthType.systemManaged;
_isRefreshing = false;
});
} else {
@@ -107,7 +109,8 @@ class _SettingsPageState extends State {
_isLoading = true;
});
- final result = await Health().hasPermissions(types);
+ final result =
+ await Health().hasPermissions(types, permissions: permissions);
if (result == true) {
setState(() {
@@ -192,7 +195,8 @@ class _SettingsPageState extends State {
],
),
// Health panel or loading box
- SizedBox( // Replace Expanded with SizedBox
+ SizedBox(
+ // Replace Expanded with SizedBox
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (Widget child, Animation animation) {
@@ -200,144 +204,179 @@ class _SettingsPageState extends State {
},
child: _isLoading
? LayoutBuilder(builder: (context, constraints) {
- final width = getWidth(constraints);
- return SizedBox(
- width: width,
- child: Column(
- children: [
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 14, vertical: 6),
- child: LoadingBox(
- height: 195,
- width: MediaQuery.of(context).size.width,
- radius: 12,
+ final width = getWidth(constraints);
+ return SizedBox(
+ width: width,
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 14, vertical: 6),
+ child: LoadingBox(
+ height: 195,
+ width: MediaQuery.of(context).size.width,
+ radius: 12,
+ ),
+ )
+ ],
),
- )
- ],
- ),
- );
- })
+ );
+ })
: buildCard([
- Text(
- _isAvailable
- ? (healthType != null
- ? "Health connected via ${HealthTypeManager.formatType(healthType)}"
- : "Connect a health platform")
- : "Health unavailable",
- style: theme.textTheme.titleLarge,
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 5),
- if (healthType == null)
- const Text(
- "Connect a health platform to create and join challenges",
- textAlign: TextAlign.center,
- )
- else if (health.steps != null)
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Symbols.steps_rounded,
- color: theme.colorScheme.primary,
- ),
- const SizedBox(width: 8),
- Text("${formatNumber(health.steps as int)} steps")
- ],
- )
- else
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Symbols.refresh_rounded,
- color: theme.colorScheme.error,
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 15).copyWith(bottom: 5),
+ child: Text(
+ _isAvailable
+ ? (healthType != null
+ ? "Health connected via ${HealthTypeManager.formatType(healthType)}"
+ : "Connect a health platform")
+ : "Health unavailable",
+ style: theme.textTheme.titleLarge,
+ textAlign: TextAlign.center,
+ ),
),
- const SizedBox(width: 8),
- Text(
- getErrorText(),
- style: theme.textTheme.bodyLarge
- ?.copyWith(color: theme.colorScheme.error),
- )
- ],
- ),
- const SizedBox(height: 10),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- FilterChip(
- onDeleted: healthType != null
- ? () {
- setState(() {
- _isLoading = true;
- });
- HealthTypeManager().clearHealthType();
- setState(() {
- _isLoading = false;
- healthType = null;
- });
- }
- : null,
- onSelected: (_isAvailable
- ? _connectSystemHealthPlatform
- : null),
- label: Text(health.capabilities
- .contains(HealthType.systemManaged)
- ? ((health.isConnected &&
- healthType == HealthType.systemManaged)
- ? "Connected"
- : HealthTypeManager.formatType(
- HealthType.systemManaged))
- : "Unavailable"),
- selected: healthType == HealthType.systemManaged,
- avatar: _isSysHealthLoading &&
- health.capabilities
- .contains(HealthType.systemManaged)
- ? const CircularProgressIndicator(
- strokeWidth: 3,
- strokeCap: StrokeCap.round,
- )
- : null,
- showCheckmark: !_isSysHealthLoading,
- ),
- const SizedBox(width: 5),
- Tooltip(
- message: "Refresh",
- child: IconButton.outlined(
- onPressed: () async {
- setState(() {
- _isRefreshing = true;
- });
- await health.fetchHealthData();
- await health.checkConnectionState();
- setState(() {
- _isRefreshing = false;
- });
- },
- icon: _isRefreshing
- ? const SizedBox(
- width: 15,
- height: 15,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- strokeCap: StrokeCap.round,
+ const SizedBox(height: 5),
+ if (healthType == null)
+ const Text(
+ "Connect a health platform to create and join challenges",
+ textAlign: TextAlign.center,
+ )
+ else if (health.steps != null)
+ Wrap(
+ spacing: 1, // Space between items horizontally
+ runSpacing: 5, // Set this to 0 to minimize space between rows
+ alignment: WrapAlignment.start, // Align items at the start
+ children: [
+ buildDataBlock(BingoDataType.steps, health.steps!, theme),
+ buildDataBlock(BingoDataType.calories, health.calories!, theme),
+ buildDataBlock(BingoDataType.distance, health.distance!, theme),
+ buildDataBlock(BingoDataType.water, health.water!, theme),
+ ],
+ )
+ else
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Symbols.refresh_rounded,
+ color: theme.colorScheme.error,
),
+ const SizedBox(width: 8),
+ Text(
+ getErrorText(),
+ style: theme.textTheme.bodyLarge
+ ?.copyWith(color: theme.colorScheme.error),
+ )
+ ],
+ ),
+ const SizedBox(height: 5),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ FilterChip(
+ onDeleted: healthType != null
+ ? () {
+ setState(() {
+ _isLoading = true;
+ });
+ HealthTypeManager().clearHealthType();
+ setState(() {
+ _isLoading = false;
+ healthType = null;
+ });
+ }
+ : null,
+ onSelected: (_isAvailable
+ ? _connectSystemHealthPlatform
+ : null),
+ label: Text(health.capabilities
+ .contains(HealthType.systemManaged)
+ ? ((health.isConnected &&
+ healthType == HealthType.systemManaged)
+ ? "Connected"
+ : HealthTypeManager.formatType(
+ HealthType.systemManaged))
+ : "Unavailable"),
+ selected: healthType == HealthType.systemManaged,
+ avatar: _isSysHealthLoading &&
+ health.capabilities
+ .contains(HealthType.systemManaged)
+ ? const CircularProgressIndicator(
+ strokeWidth: 3,
+ strokeCap: StrokeCap.round,
+ )
+ : null,
+ showCheckmark: !_isSysHealthLoading,
+ ),
+ const SizedBox(width: 5),
+ Tooltip(
+ message: "Refresh",
+ child: IconButton.outlined(
+ onPressed: () async {
+ setState(() {
+ _isRefreshing = true;
+ });
+ await health.fetchHealthData();
+ await health.checkConnectionState();
+ setState(() {
+ _isRefreshing = false;
+ });
+ },
+ icon: _isRefreshing
+ ? const SizedBox(
+ width: 15,
+ height: 15,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ strokeCap: StrokeCap.round,
+ ),
+ )
+ : Icon(
+ Symbols.refresh_rounded,
+ color: theme.colorScheme.onSurface,
+ )),
)
- : Icon(
- Symbols.refresh_rounded,
- color: theme.colorScheme.onSurface,
- )),
- )
- ],
- )
- ], height: 195),
+ ],
+ )
+ ], height: 195),
),
),
],
),
);
+ }
+ Widget buildDataBlock(BingoDataType type, num amount, ThemeData theme) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), // Minimal padding
+ margin: const EdgeInsets.all(2), // Minimal margin
+ decoration: BoxDecoration(
+ color: theme.colorScheme.surfaceContainerLow,
+ borderRadius: BorderRadius.circular(15),
+ border: Border.all(
+ color: theme.colorScheme.surfaceContainerHigh,
+ width: 1.1,
+ style: BorderStyle.solid,
+ strokeAlign: BorderSide.strokeAlignCenter,
+ )
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min, // Minimize row size
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Icon(
+ type.asIcon(),
+ color: theme.colorScheme.primary,
+ ),
+ const SizedBox(width: 4), // Smaller space between icon and text
+ Text(
+ "${formatNumber(amount)} ${type.asString()}",
+ textAlign: TextAlign.center, // Center the text
+ ),
+ ],
+ ),
+ );
}
String getErrorText() {
diff --git a/lib/utils/bingo/data.dart b/lib/utils/bingo/data.dart
index d094583..acb89f7 100644
--- a/lib/utils/bingo/data.dart
+++ b/lib/utils/bingo/data.dart
@@ -5,10 +5,10 @@ import '../../types/challenges.dart';
import '../manager.dart';
import 'manager.dart';
-enum BingoDataType { filled, steps, distance, azm }
+enum BingoDataType { filled, steps, distance, azm, calories, water }
extension BingoDataTypeExtension on BingoDataType {
- BingoDataActivity toActivity(int amount) {
+ BingoDataActivity toActivity(num amount) {
return BingoDataActivity(type: this, amount: amount);
}
@@ -17,15 +17,29 @@ extension BingoDataTypeExtension on BingoDataType {
BingoDataType.steps => Symbols.steps_rounded,
BingoDataType.distance => Symbols.distance_rounded,
BingoDataType.azm => Symbols.azm_rounded,
- BingoDataType.filled => Symbols.check_rounded,
+ BingoDataType.filled => Symbols.check_rounded,
+ BingoDataType.calories => Symbols.local_fire_department_rounded,
+ BingoDataType.water => Symbols.water_full_rounded,
_ => Symbols.indeterminate_question_box_rounded
};
}
+
+ String asString() {
+ return switch (this) {
+ BingoDataType.steps => "Steps",
+ BingoDataType.distance => "Distance",
+ BingoDataType.azm => "Active Minutes",
+ BingoDataType.filled => "Filled",
+ BingoDataType.calories => "Calories",
+ BingoDataType.water => "Water",
+ _ => "Unknown"
+ };
+ }
}
class BingoDataActivity {
final BingoDataType type;
- final int amount;
+ final num amount;
BingoDataActivity({required this.type, required this.amount});
@@ -33,7 +47,7 @@ class BingoDataActivity {
factory BingoDataActivity.fromJson(Map json) {
return BingoDataActivity(
type: BingoDataType.values[json['type'] as int],
- amount: json['amount'] as int,
+ amount: json['amount'], // No need to cast to int here
);
}
diff --git a/lib/utils/bingo/manager.dart b/lib/utils/bingo/manager.dart
index e70c61b..698b1ff 100644
--- a/lib/utils/bingo/manager.dart
+++ b/lib/utils/bingo/manager.dart
@@ -10,23 +10,44 @@ class Bingo {
final stepsRange = _getStepsRange(difficulty);
final distanceRange = _getDistanceRange(difficulty);
final activeMinutesRange = _getActiveMinutesRange(difficulty);
+ final caloriesRange = _getCaloriesRange(difficulty);
+ final waterRange = _getWaterRange(difficulty);
final List activities = List.generate(25, (index) {
- if (index % 3 == 0) {
- final steps = ((random.nextInt(stepsRange[1] - stepsRange[0] + 1) + stepsRange[0])
- .roundToNearest(5))
- .clamp(stepsRange[0], stepsRange[1]);
- return BingoDataType.steps.toActivity(steps);
- } else if (index % 3 == 1) {
- final distance = ((random.nextInt(distanceRange[1] - distanceRange[0] + 1) + distanceRange[0])
- .roundToNearest(5))
- .clamp(distanceRange[0], distanceRange[1]);
- return BingoDataType.distance.toActivity(distance);
- } else {
- final activeMinutes = ((random.nextInt(activeMinutesRange[1] - activeMinutesRange[0] + 1) + activeMinutesRange[0])
- .roundToNearest(5))
- .clamp(activeMinutesRange[0], activeMinutesRange[1]);
- return BingoDataType.azm.toActivity(activeMinutes);
+ switch (index % 5) {
+ case 0:
+ final steps = (random.nextInt(stepsRange[1] - stepsRange[0] + 1) + stepsRange[0])
+ .roundToNearest(50)
+ .clamp(stepsRange[0], stepsRange[1]);
+ return BingoDataType.steps.toActivity(steps);
+
+ case 1:
+ final distance = ((random.nextDouble() * (distanceRange[1] - distanceRange[0]) + distanceRange[0])
+ .roundToNearestNum(0.5)) // Use as double to keep precision
+ .clamp(distanceRange[0], distanceRange[1]);
+ return BingoDataType.distance.toActivity(distance); // Convert to int for compatibility
+
+ case 2:
+ final activeMinutes = (random.nextInt(activeMinutesRange[1] - activeMinutesRange[0] + 1) + activeMinutesRange[0])
+ .roundToNearest(5)
+ .clamp(activeMinutesRange[0], activeMinutesRange[1]);
+ return BingoDataType.azm.toActivity(activeMinutes);
+
+ case 3:
+ final calories = (random.nextInt(caloriesRange[1] - caloriesRange[0] + 1) + caloriesRange[0])
+ .roundToNearest(10)
+ .clamp(caloriesRange[0], caloriesRange[1]);
+ return BingoDataType.calories.toActivity(calories);
+
+ case 4:
+ // Only include water intake if the user is comfortable with it
+ final water = (random.nextInt(waterRange[1] - waterRange[0] + 1) + waterRange[0])
+ .roundToNearest(250)
+ .clamp(waterRange[0], waterRange[1]);
+ return BingoDataType.water.toActivity(water);
+
+ default:
+ return BingoDataType.steps.toActivity(stepsRange[0]);
}
});
@@ -37,26 +58,26 @@ class Bingo {
List _getStepsRange(Difficulty difficulty) {
switch (difficulty) {
case Difficulty.easy:
- return [1000, 5000];
+ return [500, 3000];
case Difficulty.medium:
- return [5000, 10000];
+ return [3000, 8000];
case Difficulty.hard:
- return [10000, 20000];
+ return [8000, 15000];
default:
- return [1000, 5000];
+ return [500, 3000];
}
}
- List _getDistanceRange(Difficulty difficulty) {
+ List _getDistanceRange(Difficulty difficulty) {
switch (difficulty) {
case Difficulty.easy:
- return [1, 5];
+ return [0.5, 3.0];
case Difficulty.medium:
- return [5, 10];
+ return [3.0, 8.0];
case Difficulty.hard:
- return [10, 20];
+ return [8.0, 12.0];
default:
- return [1, 5];
+ return [0.5, 3.0];
}
}
@@ -67,15 +88,45 @@ class Bingo {
case Difficulty.medium:
return [30, 60];
case Difficulty.hard:
- return [60, 120];
+ return [60, 90];
default:
return [10, 30];
}
}
+
+ List _getCaloriesRange(Difficulty difficulty) {
+ switch (difficulty) {
+ case Difficulty.easy:
+ return [50, 150];
+ case Difficulty.medium:
+ return [150, 300];
+ case Difficulty.hard:
+ return [300, 600];
+ default:
+ return [50, 150];
+ }
+ }
+
+ List _getWaterRange(Difficulty difficulty) {
+ switch (difficulty) {
+ case Difficulty.easy:
+ return [500, 1000];
+ case Difficulty.medium:
+ return [1000, 1500];
+ case Difficulty.hard:
+ return [1500, 2000];
+ default:
+ return [500, 1000];
+ }
+ }
}
extension on num {
int roundToNearest(int n) {
return (this / n).round() * n;
}
-}
+
+ double roundToNearestNum(num n) { // Return a double to preserve precision
+ return (this / n).roundToDouble() * n; // Use roundToDouble() to avoid potential errors
+ }
+}
\ No newline at end of file
diff --git a/lib/utils/common.dart b/lib/utils/common.dart
index 651aa2b..cc5c0a5 100644
--- a/lib/utils/common.dart
+++ b/lib/utils/common.dart
@@ -1,5 +1,9 @@
import 'package:intl/intl.dart';
-String formatNumber(int number) {
+String formatInt(int number) {
+ return NumberFormat.decimalPattern().format(number);
+}
+
+String formatNumber(num number) {
return NumberFormat.decimalPattern().format(number);
}
diff --git a/lib/utils/health.dart b/lib/utils/health.dart
index c427a96..3d42a79 100644
--- a/lib/utils/health.dart
+++ b/lib/utils/health.dart
@@ -7,7 +7,8 @@ import 'package:fitness_challenges/types/collections.dart';
import 'package:fitness_challenges/utils/data_source_manager.dart';
import 'package:fitness_challenges/utils/sharedLogger.dart';
import 'package:fitness_challenges/utils/steps/data.dart';
-import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_health_connect/flutter_health_connect.dart';
import 'package:flutter_wear_os_connectivity/flutter_wear_os_connectivity.dart';
import 'package:health/health.dart';
import 'package:pocketbase/pocketbase.dart';
@@ -30,6 +31,18 @@ class HealthManager with ChangeNotifier {
int? get activeMinutes => _azm;
+ num? _calories;
+
+ num? get calories => _calories;
+
+ num? _water;
+
+ num? get water => _water;
+
+ num? _distance;
+
+ num? get distance => _distance;
+
bool _isConnected = false;
bool get isConnected => _isConnected;
@@ -77,14 +90,33 @@ class HealthManager with ChangeNotifier {
notifyListeners();
}
+ Future getDataFromType(DateTime start, HealthDataType type) async {
+ var points = await Health().getHealthDataFromTypes(
+ startTime: start, endTime: DateTime.now(), types: [type]);
+ final data = Health().removeDuplicates(points).map((it) {
+ final value = it.value;
+ if (value is NumericHealthValue) {
+ return value.numericValue;
+ } else {
+ return 0;
+ }
+ });
+
+ if (data.isEmpty) {
+ return 0; // Return 0 if data is empty
+ } else {
+ return data.reduce((value, element) => value + element);
+ }
+ }
+
/// Attempts to fetch health data if all permissions are granted
- Future fetchHealthData({BuildContext? context}) async {
+ Future fetchHealthData({BuildContext? context}) async {
final health = Health();
final type = await HealthTypeManager().getHealthType();
if (!pb.authStore.isValid) {
logger.debug("Pocketbase auth store not valid");
- return;
+ return false;
}
var userId = pb.authStore.model?.id;
@@ -96,7 +128,15 @@ class HealthManager with ChangeNotifier {
.every((item) => item == true);
if (!isAvailable) {
logger.debug("Missing some health types");
- return;
+ HealthTypeManager().clearHealthType();
+ if(context != null && context.mounted){
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Sync failed: Missing data types'),
+ ),
+ );
+ }
+ return false;
}
final hasPermissions = await Future.wait(types.map((type) async {
@@ -111,12 +151,28 @@ class HealthManager with ChangeNotifier {
if (hasPermissions == true) {
var now = DateTime.now();
var midnight = DateTime(now.year, now.month, now.day);
+ final startTime = midnight;
_steps = await Health().getTotalStepsInInterval(midnight, now);
+ _calories = await getDataFromType(startTime, HealthDataType.TOTAL_CALORIES_BURNED);
+ _water = await getDataFromType(startTime, HealthDataType.WATER);
+ _distance = await getDataFromType(startTime, HealthDataType.DISTANCE_DELTA);
_isConnected = true;
notifyListeners(); // Notify listeners about the change
- logger.debug("Successfully synced $_steps steps");
+ logger.debug("Successfully synced:\n\nš Steps: $steps\nš„ Calories: $calories\nš¦ Water: $water\nš Distance: $distance");
} else {
logger.debug("No health permissions, sync failed");
+ HealthTypeManager().clearHealthType();
+ if(context != null && context.mounted){
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: const Text('Sync failed: Missing permissions'),
+ action: SnackBarAction(label: "Fix", onPressed: () async {
+ await HealthConnectFactory.openHealthConnectSettings();
+ }),
+ ),
+ );
+ }
+ return false;
}
} else if (type == HealthType.watch && Platform.isAndroid) {
try {
@@ -127,7 +183,7 @@ class HealthManager with ChangeNotifier {
var devices = await flutterWearOsConnectivity.getConnectedDevices();
if (devices.isEmpty) {
logger.debug("No connected devices");
- return;
+ return false;
}
for (var device in devices) {
@@ -149,7 +205,8 @@ class HealthManager with ChangeNotifier {
if (devices.isNotEmpty) _isConnected = true;
if (data.isEmpty) {
- return debugPrint("No steps from today");
+ logger.debug("No data from Wear OS client");
+ return false;
}
if (data?.first?.mapData[id] != null) {
@@ -163,11 +220,13 @@ class HealthManager with ChangeNotifier {
logger.debug("Synced $_steps steps from Wear OS client");
} else {
logger.debug("No steps from today using Wear OS client");
+ return false;
}
}
} catch (e, stacktrace) {
logger.error("Error fetching health data from Wear OS: $e");
logger.error(stacktrace.toString());
+ return false;
}
}
@@ -210,6 +269,8 @@ class HealthManager with ChangeNotifier {
await Future.delayed(const Duration(seconds: 2));
await challengeProvider.reloadChallenges(context);
}
+
+ return true;
}
DataSource getSource(HealthType type) {
diff --git a/pubspec.lock b/pubspec.lock
index a10f2ee..9277dc0 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -269,6 +269,15 @@ packages:
url: "https://github.com/Turtlepaw/flutter_emoji_feedback.git"
source: git
version: "0.3.2"
+ flutter_health_connect:
+ dependency: "direct main"
+ description:
+ path: "."
+ ref: HEAD
+ resolved-ref: fcba88afe7afcc887519d3bbb2d965419bdf48a2
+ url: "https://github.com/quentinleguennec/flutter_health_connect.git"
+ source: git
+ version: "4.1.0"
flutter_image_stack:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index fec04ee..3dacebe 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -89,6 +89,9 @@ dependencies:
pair: ^0.1.2
receive_intent: ^0.2.5
flutter_statusbarcolor_ns: ^0.6.0
+ flutter_health_connect:
+ git:
+ url: https://github.com/quentinleguennec/flutter_health_connect.git
#share_plus: 10.0.0
dev_dependencies: