Compare commits
1 Commits
3.5.0+165
...
features/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
dd6bc3cc99
|
@@ -1653,5 +1653,79 @@
|
|||||||
"dashboardCardPostsColumnDescription": "Featured Posts",
|
"dashboardCardPostsColumnDescription": "Featured Posts",
|
||||||
"dashboardCardSocialColumnDescription": "Friends & Notifications",
|
"dashboardCardSocialColumnDescription": "Friends & Notifications",
|
||||||
"dashboardCardChatsColumnDescription": "Recent Chats",
|
"dashboardCardChatsColumnDescription": "Recent Chats",
|
||||||
"searchAccountsHint": "Search across the Solar Network and fediverse network."
|
"searchAccountsHint": "Search across the Solar Network and fediverse network.",
|
||||||
}
|
"fitnessActivity": "Fitness Activity",
|
||||||
|
"loadingFitnessData": "Loading fitness data...",
|
||||||
|
"fitnessDataNotAvailable": "Fitness Data Not Available",
|
||||||
|
"fitnessDataNotAvailableDescription": "Fitness data is only available on iOS and Android devices.",
|
||||||
|
"fitnessPermissionRequired": "Permission Required",
|
||||||
|
"fitnessPermissionRequiredDescription": "To access your fitness data, we need permission to read your health information.",
|
||||||
|
"requestPermission": "Request Permission",
|
||||||
|
"noFitnessData": "No Fitness Data",
|
||||||
|
"noFitnessDataDescription": "No fitness data found. Start tracking your workouts to see them here.",
|
||||||
|
"totalWorkouts": "Total Workouts",
|
||||||
|
"totalDuration": "Total Duration",
|
||||||
|
"totalCalories": "Total Calories",
|
||||||
|
"noWorkoutsOnDate": "No workouts on {}",
|
||||||
|
"noWorkoutsOnDateDescription": "No workouts found for this date.",
|
||||||
|
"workoutsOnDate": "Workouts on {}",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"fitnessWorkoutTypeBadminton": "Badminton",
|
||||||
|
"fitnessWorkoutTypeBaseball": "Baseball",
|
||||||
|
"fitnessWorkoutTypeBasketball": "Basketball",
|
||||||
|
"fitnessWorkoutTypeBiking": "Biking",
|
||||||
|
"fitnessWorkoutTypeCalisthenics": "Calisthenics",
|
||||||
|
"fitnessWorkoutTypeCricket": "Cricket",
|
||||||
|
"fitnessWorkoutTypeDancing": "Dancing",
|
||||||
|
"fitnessWorkoutTypeElliptical": "Elliptical",
|
||||||
|
"fitnessWorkoutTypeFencing": "Fencing",
|
||||||
|
"fitnessWorkoutTypeFrisbeeDisc": "Frisbee Disc",
|
||||||
|
"fitnessWorkoutTypeGolf": "Golf",
|
||||||
|
"fitnessWorkoutTypeGymnastics": "Gymnastics",
|
||||||
|
"fitnessWorkoutTypeHiking": "Hiking",
|
||||||
|
"fitnessWorkoutTypeHockey": "Hockey",
|
||||||
|
"fitnessWorkoutTypeJumpRope": "Jump Rope",
|
||||||
|
"fitnessWorkoutTypeKickboxing": "Kickboxing",
|
||||||
|
"fitnessWorkoutTypeLacrosse": "Lacrosse",
|
||||||
|
"fitnessWorkoutTypeMartialArts": "Martial Arts",
|
||||||
|
"fitnessWorkoutTypeMindAndBody": "Mind and Body",
|
||||||
|
"fitnessWorkoutTypePilates": "Pilates",
|
||||||
|
"fitnessWorkoutTypeRowing": "Rowing",
|
||||||
|
"fitnessWorkoutTypeRowingMachine": "Rowing Machine",
|
||||||
|
"fitnessWorkoutTypeRugby": "Rugby",
|
||||||
|
"fitnessWorkoutTypeRunning": "Running",
|
||||||
|
"fitnessWorkoutTypeSailing": "Sailing",
|
||||||
|
"fitnessWorkoutTypeSkiing": "Skiing",
|
||||||
|
"fitnessWorkoutTypeSnowSports": "Snow Sports",
|
||||||
|
"fitnessWorkoutTypeSoftball": "Softball",
|
||||||
|
"fitnessWorkoutTypeStairClimbing": "Stair Climbing",
|
||||||
|
"fitnessWorkoutTypeStairClimbingMachine": "Stair Climbing Machine",
|
||||||
|
"fitnessWorkoutTypeStepTraining": "Step Training",
|
||||||
|
"fitnessWorkoutTypeSurfing": "Surfing",
|
||||||
|
"fitnessWorkoutTypeSwimming": "Swimming",
|
||||||
|
"fitnessWorkoutTypeTableTennis": "Table Tennis",
|
||||||
|
"fitnessWorkoutTypeTennis": "Tennis",
|
||||||
|
"fitnessWorkoutTypeCrossTraining": "Cross Training",
|
||||||
|
"fitnessWorkoutTypeCurling": "Curling",
|
||||||
|
"fitnessWorkoutTypeCrossCountrySkiing": "Cross Country Skiing",
|
||||||
|
"fitnessWorkoutTypeEquestrianSports": "Equestrian Sports",
|
||||||
|
"fitnessWorkoutTypeFishing": "Fishing",
|
||||||
|
"fitnessWorkoutTypeFunctionalStrengthTraining": "Functional Strength Training",
|
||||||
|
"fitnessWorkoutTypeHandCycling": "Hand Cycling",
|
||||||
|
"fitnessWorkoutTypeMixedCardio": "Mixed Cardio",
|
||||||
|
"fitnessWorkoutTypeOther": "Other",
|
||||||
|
"fitnessWorkoutTypePaddleSports": "Paddle Sports",
|
||||||
|
"fitnessWorkoutTypePickleball": "Pickleball",
|
||||||
|
"fitnessWorkoutTypeRacquetball": "Racquetball",
|
||||||
|
"fitnessWorkoutTypeRockClimbing": "Rock Climbing",
|
||||||
|
"fitnessWorkoutTypeSkating": "Skating",
|
||||||
|
"fitnessWorkoutTypeSnowboarding": "Snowboarding",
|
||||||
|
"fitnessWorkoutTypeSoccer": "Soccer",
|
||||||
|
"fitnessWorkoutTypeSquash": "Squash",
|
||||||
|
"fitnessWorkoutTypeStrengthTraining": "Strength Training",
|
||||||
|
"fitnessWorkoutTypeVolleyball": "Volleyball",
|
||||||
|
"fitnessWorkoutTypeWalking": "Walking",
|
||||||
|
"fitnessWorkoutTypeWeightlifting": "Weightlifting",
|
||||||
|
"fitnessWorkoutTypeYoga": "Yoga",
|
||||||
|
"fitnessWorkoutTypeDefault": "Fitness"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.11.0)
|
- Alamofire (5.11.1)
|
||||||
- audio_session (0.0.1):
|
- audio_session (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
@@ -213,6 +213,8 @@ PODS:
|
|||||||
- GoogleUtilities/UserDefaults (8.1.0):
|
- GoogleUtilities/UserDefaults (8.1.0):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
|
- health (13.1.4):
|
||||||
|
- Flutter
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
@@ -354,6 +356,7 @@ DEPENDENCIES:
|
|||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||||
|
- health (from `.symlinks/plugins/health/ios`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||||
@@ -460,6 +463,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||||
gal:
|
gal:
|
||||||
:path: ".symlinks/plugins/gal/darwin"
|
:path: ".symlinks/plugins/gal/darwin"
|
||||||
|
health:
|
||||||
|
:path: ".symlinks/plugins/health/ios"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
in_app_review:
|
in_app_review:
|
||||||
@@ -518,7 +523,7 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: bd5e7b23a1a750975288482c1831d71e74415f86
|
Alamofire: eec6cd8f73b242b59e34153a606a909eb9864b14
|
||||||
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
|
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||||
@@ -556,6 +561,7 @@ SPEC CHECKSUMS:
|
|||||||
GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17
|
GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
|
health: 32d2fbc7f26f9a2388d1a514ce168adbfa5bda65
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
|
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
|
||||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -222,8 +222,6 @@
|
|||||||
};
|
};
|
||||||
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
|
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = "Solian Watch App";
|
path = "Solian Watch App";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -771,10 +769,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
@@ -832,10 +834,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
@@ -886,10 +892,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";
|
||||||
|
|||||||
@@ -1,122 +1,127 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>AppGroupId</key>
|
||||||
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>dev.solsynth.solian</string>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Solian</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>solian</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string></string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>solian</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>CLIENT_ID</key>
|
||||||
|
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSAppIntentsConfiguration</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>AppGroupId</key>
|
<key>NSAppIntentsPackage</key>
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
|
||||||
<key>BUNDLE_ID</key>
|
|
||||||
<string>dev.solsynth.solian</string>
|
<string>dev.solsynth.solian</string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
||||||
<true/>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>Solian</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>solian</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Editor</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Editor</string>
|
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string></string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>solian</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
|
||||||
<key>CLIENT_ID</key>
|
|
||||||
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSCalendarsUsageDescription</key>
|
|
||||||
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
|
||||||
<key>NSFaceIDUsageDescription</key>
|
|
||||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
|
||||||
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
|
||||||
<key>NSSpeechRecognitionUsageDescription</key>
|
|
||||||
<string>Solian uses speech recognition for Siri integration</string>
|
|
||||||
<key>NSAppIntentsConfiguration</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAppIntentsPackage</key>
|
|
||||||
<string>dev.solsynth.solian</string>
|
|
||||||
</dict>
|
|
||||||
<key>NSAppIntentsMetadata</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAppIntentsSupported</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
|
||||||
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
|
||||||
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
|
|
||||||
<key>NSUserActivityTypes</key>
|
|
||||||
<array>
|
|
||||||
<string>INStartCallIntent</string>
|
|
||||||
<string>INSendMessageIntent</string>
|
|
||||||
</array>
|
|
||||||
<key>PLIST_VERSION</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>REVERSED_CLIENT_ID</key>
|
|
||||||
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
|
||||||
<true/>
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>fetch</string>
|
|
||||||
<string>audio</string>
|
|
||||||
<string>remote-notification</string>
|
|
||||||
<string>voip</string>
|
|
||||||
</array>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UIStatusBarHidden</key>
|
|
||||||
<false/>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
</array>
|
|
||||||
<key>WKCompanionAppBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>NSAppIntentsMetadata</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAppIntentsSupported</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>NSCalendarsUsageDescription</key>
|
||||||
|
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
||||||
|
<key>NSFaceIDUsageDescription</key>
|
||||||
|
<string>Allow the Solar Network verify your ownership of the logged in account and continue
|
||||||
|
your action quickly.</string>
|
||||||
|
<key>NSHealthShareUsageDescription</key>
|
||||||
|
<string>Allow us to share your fitness data with your friends.</string>
|
||||||
|
<key>NSHealthUpdateUsageDescription</key>
|
||||||
|
<string>Allow us to update your fitness data with your friends.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
|
||||||
|
<key>NSSpeechRecognitionUsageDescription</key>
|
||||||
|
<string>Solian uses speech recognition for Siri integration</string>
|
||||||
|
<key>NSUserActivityTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>INStartCallIntent</string>
|
||||||
|
<string>INSendMessageIntent</string>
|
||||||
|
</array>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
|
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>audio</string>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
<string>voip</string>
|
||||||
|
</array>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UIStatusBarHidden</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
</array>
|
||||||
|
<key>WKCompanionAppBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>com.apple.developer.device-information.user-assigned-device-name</key>
|
<key>com.apple.developer.device-information.user-assigned-device-name</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.developer.healthkit</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.developer.usernotifications.communication</key>
|
<key>com.apple.developer.usernotifications.communication</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import 'package:island/screens/creators/poll/poll_list.dart';
|
|||||||
import 'package:island/screens/creators/sites/site_detail.dart';
|
import 'package:island/screens/creators/sites/site_detail.dart';
|
||||||
import 'package:island/screens/creators/sites/site_list.dart';
|
import 'package:island/screens/creators/sites/site_list.dart';
|
||||||
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
||||||
|
import 'package:island/screens/fitness_activity.dart';
|
||||||
import 'package:island/screens/posts/compose.dart';
|
import 'package:island/screens/posts/compose.dart';
|
||||||
import 'package:island/screens/posts/compose_article.dart';
|
import 'package:island/screens/posts/compose_article.dart';
|
||||||
import 'package:island/screens/posts/post_detail.dart';
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
@@ -437,6 +438,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return AbuseReportDetailScreen(reportId: id);
|
return AbuseReportDetailScreen(reportId: id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'fitnessActivity',
|
||||||
|
path: '/account/fitness',
|
||||||
|
builder: (context, state) => const FitnessActivityScreen(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -419,6 +419,13 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
context.pushNamed('reportList');
|
context.pushNamed('reportList');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'icon': Symbols.fitness_center,
|
||||||
|
'title': 'fitnessActivity',
|
||||||
|
'onTap': () {
|
||||||
|
context.pushNamed('fitnessActivity');
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
return Column(
|
return Column(
|
||||||
children: menuItems.map((item) {
|
children: menuItems.map((item) {
|
||||||
|
|||||||
777
lib/screens/fitness_activity.dart
Normal file
777
lib/screens/fitness_activity.dart
Normal file
@@ -0,0 +1,777 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:health/health.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/services/fitness_service.dart';
|
||||||
|
import 'package:island/services/fitness_data.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
class FitnessActivityScreen extends HookConsumerWidget {
|
||||||
|
const FitnessActivityScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final fitnessService = ref.watch(fitnessServiceProvider);
|
||||||
|
final selectedDate = useState(DateTime.now());
|
||||||
|
final workouts = useState<List<FitnessWorkout>>([]);
|
||||||
|
final isLoading = useState(false);
|
||||||
|
final hasPermission = useState(false);
|
||||||
|
final isDataAvailable = useState(false);
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
useEffect(() {
|
||||||
|
_loadData(
|
||||||
|
fitnessService,
|
||||||
|
selectedDate.value,
|
||||||
|
workouts,
|
||||||
|
isLoading,
|
||||||
|
hasPermission,
|
||||||
|
isDataAvailable,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('fitnessActivity').tr(),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.refresh),
|
||||||
|
onPressed: () {
|
||||||
|
_loadData(
|
||||||
|
fitnessService,
|
||||||
|
selectedDate.value,
|
||||||
|
workouts,
|
||||||
|
isLoading,
|
||||||
|
hasPermission,
|
||||||
|
isDataAvailable,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tooltip: 'refresh'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: isLoading.value
|
||||||
|
? _buildLoadingView()
|
||||||
|
: _buildMainContent(
|
||||||
|
context,
|
||||||
|
fitnessService,
|
||||||
|
selectedDate,
|
||||||
|
workouts,
|
||||||
|
hasPermission,
|
||||||
|
isDataAvailable,
|
||||||
|
(date) {
|
||||||
|
selectedDate.value = date;
|
||||||
|
_loadData(
|
||||||
|
fitnessService,
|
||||||
|
date,
|
||||||
|
workouts,
|
||||||
|
isLoading,
|
||||||
|
hasPermission,
|
||||||
|
isDataAvailable,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingView() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('loadingFitnessData').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMainContent(
|
||||||
|
BuildContext context,
|
||||||
|
FitnessService fitnessService,
|
||||||
|
ValueNotifier<DateTime> selectedDate,
|
||||||
|
ValueNotifier<List<FitnessWorkout>> workouts,
|
||||||
|
ValueNotifier<bool> hasPermission,
|
||||||
|
ValueNotifier<bool> isDataAvailable,
|
||||||
|
ValueChanged<DateTime> onDateSelected,
|
||||||
|
) {
|
||||||
|
if (!fitnessService.isPlatformSupported) {
|
||||||
|
return _buildUnsupportedPlatformView();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission.value) {
|
||||||
|
return _buildPermissionDeniedView(fitnessService);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDataAvailable.value) {
|
||||||
|
return _buildNoDataView();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Calendar View
|
||||||
|
_buildCalendarView(
|
||||||
|
context,
|
||||||
|
selectedDate,
|
||||||
|
workouts.value,
|
||||||
|
onDateSelected,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Summary Cards
|
||||||
|
_buildSummaryCards(workouts.value),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Selected Date Workouts
|
||||||
|
_buildSelectedDateWorkouts(
|
||||||
|
selectedDate.value,
|
||||||
|
workouts.value,
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUnsupportedPlatformView() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.fitness_center, size: 64, color: Colors.grey[600]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'fitnessDataNotAvailable',
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'fitnessDataNotAvailableDescription',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPermissionDeniedView(FitnessService fitnessService) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.block, size: 64, color: Colors.red[600]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'fitnessPermissionRequired',
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'fitnessPermissionRequiredDescription',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Symbols.settings),
|
||||||
|
label: Text('requestPermission').tr(),
|
||||||
|
onPressed: () async {
|
||||||
|
final granted = await fitnessService.requestPermissions();
|
||||||
|
if (granted) {
|
||||||
|
// Reload data
|
||||||
|
// This would need to be handled by the parent widget
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNoDataView() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.fitness_center, size: 64, color: Colors.grey[600]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'noFitnessData',
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('noFitnessDataDescription', textAlign: TextAlign.center).tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCalendarView(
|
||||||
|
BuildContext context,
|
||||||
|
ValueNotifier<DateTime> selectedDate,
|
||||||
|
List<FitnessWorkout> workouts,
|
||||||
|
ValueChanged<DateTime> onDateSelected,
|
||||||
|
) {
|
||||||
|
// Create a map of dates with workout data
|
||||||
|
final workoutData = <DateTime, List<FitnessWorkout>>{};
|
||||||
|
for (final workout in workouts) {
|
||||||
|
final date = DateTime(
|
||||||
|
workout.startTime.year,
|
||||||
|
workout.startTime.month,
|
||||||
|
workout.startTime.day,
|
||||||
|
);
|
||||||
|
if (!workoutData.containsKey(date)) {
|
||||||
|
workoutData[date] = [];
|
||||||
|
}
|
||||||
|
workoutData[date]!.add(workout);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: TableCalendar<FitnessWorkout>(
|
||||||
|
firstDay: DateTime(2020),
|
||||||
|
lastDay: DateTime.now(),
|
||||||
|
focusedDay: selectedDate.value,
|
||||||
|
selectedDayPredicate: (day) {
|
||||||
|
return isSameDay(day, selectedDate.value);
|
||||||
|
},
|
||||||
|
onDaySelected: (selectedDay, focusedDay) {
|
||||||
|
onDateSelected(selectedDay);
|
||||||
|
},
|
||||||
|
calendarBuilders: CalendarBuilders(
|
||||||
|
markerBuilder: (context, date, events) {
|
||||||
|
if (workoutData.containsKey(date)) {
|
||||||
|
final dailyWorkouts = workoutData[date]!;
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: _buildFitnessMarker(dailyWorkouts),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
todayBuilder: (context, date, _) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
date.day.toString(),
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
calendarStyle: CalendarStyle(
|
||||||
|
selectedDecoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
todayDecoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryCards(List<FitnessWorkout> workouts) {
|
||||||
|
if (workouts.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate summary statistics
|
||||||
|
final totalWorkouts = workouts.length;
|
||||||
|
final totalDuration = workouts.fold(
|
||||||
|
Duration.zero,
|
||||||
|
(sum, workout) => sum + workout.endTime.difference(workout.startTime),
|
||||||
|
);
|
||||||
|
final totalCalories = workouts.fold(
|
||||||
|
0.0,
|
||||||
|
(sum, workout) => sum + (workout.totalEnergyBurned ?? 0),
|
||||||
|
);
|
||||||
|
final totalDistance = workouts.fold(
|
||||||
|
0.0,
|
||||||
|
(sum, workout) => sum + (workout.totalDistance ?? 0),
|
||||||
|
);
|
||||||
|
final totalSteps = workouts.fold(
|
||||||
|
0.0,
|
||||||
|
(sum, workout) => sum + (workout.totalSteps ?? 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
_buildSummaryCard(
|
||||||
|
icon: Symbols.fitness_center,
|
||||||
|
title: 'totalWorkouts',
|
||||||
|
value: totalWorkouts.toString(),
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
_buildSummaryCard(
|
||||||
|
icon: Symbols.timer,
|
||||||
|
title: 'totalDuration',
|
||||||
|
value: _formatDuration(totalDuration),
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
_buildSummaryCard(
|
||||||
|
icon: Symbols.local_fire_department,
|
||||||
|
title: 'totalCalories',
|
||||||
|
value: '${totalCalories.toInt()} kcal',
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryCard({
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required String value,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color, size: 24),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSelectedDateWorkouts(
|
||||||
|
DateTime selectedDate,
|
||||||
|
List<FitnessWorkout> workouts,
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
// Filter workouts for selected date
|
||||||
|
final dateWorkouts = workouts.where((workout) {
|
||||||
|
final workoutDate = DateTime(
|
||||||
|
workout.startTime.year,
|
||||||
|
workout.startTime.month,
|
||||||
|
workout.startTime.day,
|
||||||
|
);
|
||||||
|
return isSameDay(workoutDate, selectedDate);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (dateWorkouts.isEmpty) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.calendar_month, size: 48, color: Colors.grey[600]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'noWorkoutsOnDate',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(args: [DateFormat('MMM d, yyyy').format(selectedDate)]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('noWorkoutsOnDateDescription').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
'workoutsOnDate',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
).tr(args: [DateFormat('MMM d, yyyy').format(selectedDate)]),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListView.separated(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: dateWorkouts.length,
|
||||||
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final workout = dateWorkouts[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: _getWorkoutIcon(workout.workoutType),
|
||||||
|
title: Text(workout.workoutTypeString),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${DateFormat('HH:mm').format(workout.startTime)} - ${DateFormat('HH:mm').format(workout.endTime)}',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (workout.energyBurnedString.isNotEmpty)
|
||||||
|
_buildStatChip(
|
||||||
|
icon: Symbols.local_fire_department,
|
||||||
|
text: workout.energyBurnedString,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
if (workout.distanceString.isNotEmpty)
|
||||||
|
_buildStatChip(
|
||||||
|
icon: Symbols.straighten,
|
||||||
|
text: workout.distanceString,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
if (workout.stepsString.isNotEmpty)
|
||||||
|
_buildStatChip(
|
||||||
|
icon: Symbols.directions_walk,
|
||||||
|
text: '${workout.stepsString} steps',
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Text(workout.durationString),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatChip({
|
||||||
|
required IconData icon,
|
||||||
|
required String text,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
margin: const EdgeInsets.only(right: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: color),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(text, style: TextStyle(fontSize: 12, color: color)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getWorkoutIcon(HealthWorkoutActivityType type) {
|
||||||
|
switch (type) {
|
||||||
|
case HealthWorkoutActivityType.RUNNING:
|
||||||
|
return const Icon(Symbols.directions_run, color: Colors.blue);
|
||||||
|
case HealthWorkoutActivityType.WALKING:
|
||||||
|
return const Icon(Symbols.directions_walk, color: Colors.green);
|
||||||
|
case HealthWorkoutActivityType.BIKING:
|
||||||
|
return const Icon(Symbols.directions_bike, color: Colors.orange);
|
||||||
|
case HealthWorkoutActivityType.SWIMMING:
|
||||||
|
return const Icon(Symbols.pool, color: Colors.cyan);
|
||||||
|
case HealthWorkoutActivityType.STRENGTH_TRAINING:
|
||||||
|
case HealthWorkoutActivityType.WEIGHTLIFTING:
|
||||||
|
return const Icon(Symbols.fitness_center, color: Colors.red);
|
||||||
|
case HealthWorkoutActivityType.YOGA:
|
||||||
|
return const Icon(Symbols.self_improvement, color: Colors.purple);
|
||||||
|
default:
|
||||||
|
return const Icon(Symbols.fitness_center, color: Colors.grey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDuration(Duration duration) {
|
||||||
|
final hours = duration.inHours;
|
||||||
|
final minutes = duration.inMinutes.remainder(60);
|
||||||
|
if (hours > 0) {
|
||||||
|
return '${hours}h ${minutes}m';
|
||||||
|
} else {
|
||||||
|
return '${minutes}m';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFitnessMarker(List<FitnessWorkout> workouts) {
|
||||||
|
// If no workouts, show nothing
|
||||||
|
if (workouts.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For better visibility, we'll use a simple colored dot with workout count
|
||||||
|
// This is more visible than the complex ring for small calendar cells
|
||||||
|
return Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withOpacity(0.8),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black26,
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
workouts.length.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadData(
|
||||||
|
FitnessService fitnessService,
|
||||||
|
DateTime date,
|
||||||
|
ValueNotifier<List<FitnessWorkout>> workouts,
|
||||||
|
ValueNotifier<bool> isLoading,
|
||||||
|
ValueNotifier<bool> hasPermission,
|
||||||
|
ValueNotifier<bool> isDataAvailable,
|
||||||
|
) async {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check platform support
|
||||||
|
if (!fitnessService.isPlatformSupported) {
|
||||||
|
hasPermission.value = false;
|
||||||
|
isDataAvailable.value = false;
|
||||||
|
workouts.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
final permissionStatus = await fitnessService.getPermissionStatus();
|
||||||
|
hasPermission.value = permissionStatus == FitnessPermissionStatus.granted;
|
||||||
|
|
||||||
|
if (!hasPermission.value) {
|
||||||
|
isDataAvailable.value = false;
|
||||||
|
workouts.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get workouts for the last 30 days to populate calendar
|
||||||
|
final allWorkouts = await fitnessService.getWorkoutsLast30Days();
|
||||||
|
workouts.value = allWorkouts;
|
||||||
|
isDataAvailable.value = allWorkouts.isNotEmpty;
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
workouts.value = [];
|
||||||
|
isDataAvailable.value = false;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fitness ring marker that shows workout progress for a specific day
|
||||||
|
class FitnessRingMarker extends StatelessWidget {
|
||||||
|
final List<FitnessWorkout> workouts;
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
const FitnessRingMarker({super.key, required this.workouts, this.size = 24});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Calculate total stats for the day
|
||||||
|
final totalDuration = workouts.fold(
|
||||||
|
Duration.zero,
|
||||||
|
(sum, workout) => sum + workout.endTime.difference(workout.startTime),
|
||||||
|
);
|
||||||
|
final totalCalories = workouts.fold(
|
||||||
|
0.0,
|
||||||
|
(sum, workout) => sum + (workout.totalEnergyBurned ?? 0),
|
||||||
|
);
|
||||||
|
final totalDistance = workouts.fold(
|
||||||
|
0.0,
|
||||||
|
(sum, workout) => sum + (workout.totalDistance ?? 0),
|
||||||
|
);
|
||||||
|
final totalSteps = workouts.fold(
|
||||||
|
0.0,
|
||||||
|
(sum, workout) => sum + (workout.totalSteps ?? 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate progress percentages (with some reasonable goals)
|
||||||
|
final durationProgress = _calculateDurationProgress(totalDuration);
|
||||||
|
final caloriesProgress = _calculateCaloriesProgress(totalCalories);
|
||||||
|
final stepsProgress = _calculateStepsProgress(totalSteps);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
// Background ring (Steps)
|
||||||
|
_buildRing(
|
||||||
|
progress: stepsProgress,
|
||||||
|
color: Colors.green,
|
||||||
|
width: 3,
|
||||||
|
size: size,
|
||||||
|
isBackground: true,
|
||||||
|
),
|
||||||
|
// Middle ring (Calories)
|
||||||
|
_buildRing(
|
||||||
|
progress: caloriesProgress,
|
||||||
|
color: Colors.orange,
|
||||||
|
width: 3,
|
||||||
|
size: size - 4,
|
||||||
|
isBackground: true,
|
||||||
|
),
|
||||||
|
// Inner ring (Duration)
|
||||||
|
_buildRing(
|
||||||
|
progress: durationProgress,
|
||||||
|
color: Colors.blue,
|
||||||
|
width: 3,
|
||||||
|
size: size - 8,
|
||||||
|
isBackground: false,
|
||||||
|
),
|
||||||
|
// Center indicator
|
||||||
|
_buildCenterIndicator(workouts.length),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRing({
|
||||||
|
required double progress,
|
||||||
|
required Color color,
|
||||||
|
required double width,
|
||||||
|
required double size,
|
||||||
|
required bool isBackground,
|
||||||
|
}) {
|
||||||
|
return CustomPaint(
|
||||||
|
size: Size(size, size),
|
||||||
|
painter: FitnessRingPainter(
|
||||||
|
progress: progress,
|
||||||
|
color: color,
|
||||||
|
width: width,
|
||||||
|
isBackground: isBackground,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCenterIndicator(int workoutCount) {
|
||||||
|
return Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black26, blurRadius: 2, offset: Offset(0, 1)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
workoutCount.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calculateDurationProgress(Duration duration) {
|
||||||
|
// Goal: 30 minutes of exercise per day
|
||||||
|
final goalMinutes = 30.0;
|
||||||
|
final actualMinutes = duration.inMinutes.toDouble();
|
||||||
|
return (actualMinutes / goalMinutes).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calculateCaloriesProgress(double calories) {
|
||||||
|
// Goal: 250 calories burned per day
|
||||||
|
final goalCalories = 250.0;
|
||||||
|
return (calories / goalCalories).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calculateStepsProgress(double steps) {
|
||||||
|
// Goal: 5000 steps per day
|
||||||
|
final goalSteps = 5000.0;
|
||||||
|
return (steps / goalSteps).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom painter for drawing fitness rings
|
||||||
|
class FitnessRingPainter extends CustomPainter {
|
||||||
|
final double progress;
|
||||||
|
final Color color;
|
||||||
|
final double width;
|
||||||
|
final bool isBackground;
|
||||||
|
|
||||||
|
FitnessRingPainter({
|
||||||
|
required this.progress,
|
||||||
|
required this.color,
|
||||||
|
required this.width,
|
||||||
|
required this.isBackground,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
final radius = min(size.width, size.height) / 2 - width / 2;
|
||||||
|
|
||||||
|
final paint = Paint()
|
||||||
|
..color = isBackground ? color.withOpacity(0.2) : color
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = width
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
|
if (isBackground) {
|
||||||
|
// Draw full background ring
|
||||||
|
canvas.drawCircle(center, radius, paint);
|
||||||
|
} else {
|
||||||
|
// Draw progress arc
|
||||||
|
final sweepAngle = 2 * pi * progress;
|
||||||
|
final startAngle = -pi / 2; // Start from top
|
||||||
|
|
||||||
|
canvas.drawArc(
|
||||||
|
Rect.fromCircle(center: center, radius: radius),
|
||||||
|
startAngle,
|
||||||
|
sweepAngle,
|
||||||
|
false,
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
195
lib/services/fitness_data.dart
Normal file
195
lib/services/fitness_data.dart
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import 'package:health/health.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
|
/// Represents a fitness workout with structured data
|
||||||
|
class FitnessWorkout {
|
||||||
|
final DateTime startTime;
|
||||||
|
final DateTime endTime;
|
||||||
|
final HealthWorkoutActivityType workoutType;
|
||||||
|
final double? totalEnergyBurned;
|
||||||
|
final HealthDataUnit? totalEnergyBurnedUnit;
|
||||||
|
final double? totalDistance;
|
||||||
|
final HealthDataUnit? totalDistanceUnit;
|
||||||
|
final double? totalSteps;
|
||||||
|
final HealthDataUnit? totalStepsUnit;
|
||||||
|
|
||||||
|
FitnessWorkout({
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
|
required this.workoutType,
|
||||||
|
this.totalEnergyBurned,
|
||||||
|
this.totalEnergyBurnedUnit,
|
||||||
|
this.totalDistance,
|
||||||
|
this.totalDistanceUnit,
|
||||||
|
this.totalSteps,
|
||||||
|
this.totalStepsUnit,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Convert workout type to human-readable string
|
||||||
|
String get workoutTypeString {
|
||||||
|
switch (workoutType) {
|
||||||
|
case HealthWorkoutActivityType.BADMINTON:
|
||||||
|
return 'fitnessWorkoutTypeBadminton'.tr();
|
||||||
|
case HealthWorkoutActivityType.BASEBALL:
|
||||||
|
return 'fitnessWorkoutTypeBaseball'.tr();
|
||||||
|
case HealthWorkoutActivityType.BASKETBALL:
|
||||||
|
return 'fitnessWorkoutTypeBasketball'.tr();
|
||||||
|
case HealthWorkoutActivityType.BIKING:
|
||||||
|
return 'fitnessWorkoutTypeBiking'.tr();
|
||||||
|
case HealthWorkoutActivityType.CALISTHENICS:
|
||||||
|
return 'fitnessWorkoutTypeCalisthenics'.tr();
|
||||||
|
case HealthWorkoutActivityType.CRICKET:
|
||||||
|
return 'fitnessWorkoutTypeCricket'.tr();
|
||||||
|
case HealthWorkoutActivityType.DANCING:
|
||||||
|
return 'fitnessWorkoutTypeDancing'.tr();
|
||||||
|
case HealthWorkoutActivityType.ELLIPTICAL:
|
||||||
|
return 'fitnessWorkoutTypeElliptical'.tr();
|
||||||
|
case HealthWorkoutActivityType.FENCING:
|
||||||
|
return 'fitnessWorkoutTypeFencing'.tr();
|
||||||
|
case HealthWorkoutActivityType.FRISBEE_DISC:
|
||||||
|
return 'fitnessWorkoutTypeFrisbeeDisc'.tr();
|
||||||
|
case HealthWorkoutActivityType.GOLF:
|
||||||
|
return 'fitnessWorkoutTypeGolf'.tr();
|
||||||
|
case HealthWorkoutActivityType.GYMNASTICS:
|
||||||
|
return 'fitnessWorkoutTypeGymnastics'.tr();
|
||||||
|
case HealthWorkoutActivityType.HIKING:
|
||||||
|
return 'fitnessWorkoutTypeHiking'.tr();
|
||||||
|
case HealthWorkoutActivityType.HOCKEY:
|
||||||
|
return 'fitnessWorkoutTypeHockey'.tr();
|
||||||
|
case HealthWorkoutActivityType.JUMP_ROPE:
|
||||||
|
return 'fitnessWorkoutTypeJumpRope'.tr();
|
||||||
|
case HealthWorkoutActivityType.KICKBOXING:
|
||||||
|
return 'fitnessWorkoutTypeKickboxing'.tr();
|
||||||
|
case HealthWorkoutActivityType.LACROSSE:
|
||||||
|
return 'fitnessWorkoutTypeLacrosse'.tr();
|
||||||
|
case HealthWorkoutActivityType.MARTIAL_ARTS:
|
||||||
|
return 'fitnessWorkoutTypeMartialArts'.tr();
|
||||||
|
case HealthWorkoutActivityType.MIND_AND_BODY:
|
||||||
|
return 'fitnessWorkoutTypeMindAndBody'.tr();
|
||||||
|
case HealthWorkoutActivityType.PILATES:
|
||||||
|
return 'fitnessWorkoutTypePilates'.tr();
|
||||||
|
case HealthWorkoutActivityType.ROWING:
|
||||||
|
return 'fitnessWorkoutTypeRowing'.tr();
|
||||||
|
case HealthWorkoutActivityType.ROWING_MACHINE:
|
||||||
|
return 'fitnessWorkoutTypeRowingMachine'.tr();
|
||||||
|
case HealthWorkoutActivityType.RUGBY:
|
||||||
|
return 'fitnessWorkoutTypeRugby'.tr();
|
||||||
|
case HealthWorkoutActivityType.RUNNING:
|
||||||
|
return 'fitnessWorkoutTypeRunning'.tr();
|
||||||
|
case HealthWorkoutActivityType.SAILING:
|
||||||
|
return 'fitnessWorkoutTypeSailing'.tr();
|
||||||
|
case HealthWorkoutActivityType.SKIING:
|
||||||
|
return 'fitnessWorkoutTypeSkiing'.tr();
|
||||||
|
case HealthWorkoutActivityType.SNOW_SPORTS:
|
||||||
|
return 'fitnessWorkoutTypeSnowSports'.tr();
|
||||||
|
case HealthWorkoutActivityType.SOFTBALL:
|
||||||
|
return 'fitnessWorkoutTypeSoftball'.tr();
|
||||||
|
case HealthWorkoutActivityType.STAIR_CLIMBING:
|
||||||
|
return 'fitnessWorkoutTypeStairClimbing'.tr();
|
||||||
|
case HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE:
|
||||||
|
return 'fitnessWorkoutTypeStairClimbingMachine'.tr();
|
||||||
|
case HealthWorkoutActivityType.STEP_TRAINING:
|
||||||
|
return 'fitnessWorkoutTypeStepTraining'.tr();
|
||||||
|
case HealthWorkoutActivityType.SURFING:
|
||||||
|
return 'fitnessWorkoutTypeSurfing'.tr();
|
||||||
|
case HealthWorkoutActivityType.SWIMMING:
|
||||||
|
return 'fitnessWorkoutTypeSwimming'.tr();
|
||||||
|
case HealthWorkoutActivityType.TABLE_TENNIS:
|
||||||
|
return 'fitnessWorkoutTypeTableTennis'.tr();
|
||||||
|
case HealthWorkoutActivityType.TENNIS:
|
||||||
|
return 'fitnessWorkoutTypeTennis'.tr();
|
||||||
|
case HealthWorkoutActivityType.CROSS_TRAINING:
|
||||||
|
return 'fitnessWorkoutTypeCrossTraining'.tr();
|
||||||
|
case HealthWorkoutActivityType.CURLING:
|
||||||
|
return 'fitnessWorkoutTypeCurling'.tr();
|
||||||
|
case HealthWorkoutActivityType.CROSS_COUNTRY_SKIING:
|
||||||
|
return 'fitnessWorkoutTypeCrossCountrySkiing'.tr();
|
||||||
|
case HealthWorkoutActivityType.EQUESTRIAN_SPORTS:
|
||||||
|
return 'fitnessWorkoutTypeEquestrianSports'.tr();
|
||||||
|
case HealthWorkoutActivityType.FISHING:
|
||||||
|
return 'fitnessWorkoutTypeFishing'.tr();
|
||||||
|
case HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING:
|
||||||
|
return 'fitnessWorkoutTypeFunctionalStrengthTraining'.tr();
|
||||||
|
case HealthWorkoutActivityType.HAND_CYCLING:
|
||||||
|
return 'fitnessWorkoutTypeHandCycling'.tr();
|
||||||
|
case HealthWorkoutActivityType.MIXED_CARDIO:
|
||||||
|
return 'fitnessWorkoutTypeMixedCardio'.tr();
|
||||||
|
case HealthWorkoutActivityType.OTHER:
|
||||||
|
return 'fitnessWorkoutTypeOther'.tr();
|
||||||
|
case HealthWorkoutActivityType.PADDLE_SPORTS:
|
||||||
|
return 'fitnessWorkoutTypePaddleSports'.tr();
|
||||||
|
case HealthWorkoutActivityType.PICKLEBALL:
|
||||||
|
return 'fitnessWorkoutTypePickleball'.tr();
|
||||||
|
case HealthWorkoutActivityType.RACQUETBALL:
|
||||||
|
return 'fitnessWorkoutTypeRacquetball'.tr();
|
||||||
|
case HealthWorkoutActivityType.ROCK_CLIMBING:
|
||||||
|
return 'fitnessWorkoutTypeRockClimbing'.tr();
|
||||||
|
case HealthWorkoutActivityType.SKATING:
|
||||||
|
return 'fitnessWorkoutTypeSkating'.tr();
|
||||||
|
case HealthWorkoutActivityType.SNOWBOARDING:
|
||||||
|
return 'fitnessWorkoutTypeSnowboarding'.tr();
|
||||||
|
case HealthWorkoutActivityType.SOCCER:
|
||||||
|
return 'fitnessWorkoutTypeSoccer'.tr();
|
||||||
|
case HealthWorkoutActivityType.SQUASH:
|
||||||
|
return 'fitnessWorkoutTypeSquash'.tr();
|
||||||
|
case HealthWorkoutActivityType.STRENGTH_TRAINING:
|
||||||
|
return 'fitnessWorkoutTypeStrengthTraining'.tr();
|
||||||
|
case HealthWorkoutActivityType.VOLLEYBALL:
|
||||||
|
return 'fitnessWorkoutTypeVolleyball'.tr();
|
||||||
|
case HealthWorkoutActivityType.WALKING:
|
||||||
|
return 'fitnessWorkoutTypeWalking'.tr();
|
||||||
|
case HealthWorkoutActivityType.WEIGHTLIFTING:
|
||||||
|
return 'fitnessWorkoutTypeWeightlifting'.tr();
|
||||||
|
case HealthWorkoutActivityType.YOGA:
|
||||||
|
return 'fitnessWorkoutTypeYoga'.tr();
|
||||||
|
default:
|
||||||
|
return 'fitnessWorkoutTypeDefault'.tr();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get formatted duration string
|
||||||
|
String get durationString {
|
||||||
|
final duration = endTime.difference(startTime);
|
||||||
|
final hours = duration.inHours;
|
||||||
|
final minutes = duration.inMinutes.remainder(60);
|
||||||
|
final seconds = duration.inSeconds.remainder(60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return '${hours}h ${minutes}m ${seconds}s';
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return '${minutes}m ${seconds}s';
|
||||||
|
} else {
|
||||||
|
return '${seconds}s';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get formatted energy burned string
|
||||||
|
String get energyBurnedString {
|
||||||
|
if (totalEnergyBurned == null || totalEnergyBurnedUnit == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return '${totalEnergyBurned!.toStringAsFixed(2)} ${totalEnergyBurnedUnit!.name}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get formatted distance string
|
||||||
|
String get distanceString {
|
||||||
|
if (totalDistance == null || totalDistanceUnit == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return '${totalDistance!.toStringAsFixed(2)} ${totalDistanceUnit!.name}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get formatted steps string
|
||||||
|
String get stepsString {
|
||||||
|
if (totalSteps == null || totalStepsUnit == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return '${totalSteps!.toStringAsFixed(0)} ${totalStepsUnit!.name}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum for different types of fitness data
|
||||||
|
enum FitnessDataType { workout, steps, distance, calories, heartRate, sleep }
|
||||||
|
|
||||||
|
/// Enum for permission status
|
||||||
|
enum FitnessPermissionStatus { granted, denied, restricted, notDetermined }
|
||||||
353
lib/services/fitness_service.dart
Normal file
353
lib/services/fitness_service.dart
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:health/health.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
|
import 'package:island/services/fitness_data.dart';
|
||||||
|
|
||||||
|
final fitnessServiceProvider = Provider<FitnessService>((ref) {
|
||||||
|
return FitnessService();
|
||||||
|
});
|
||||||
|
|
||||||
|
class FitnessService {
|
||||||
|
final Health _health = Health();
|
||||||
|
|
||||||
|
/// Check if the platform supports fitness data
|
||||||
|
bool get isPlatformSupported {
|
||||||
|
return !kIsWeb && (Platform.isIOS || Platform.isAndroid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request permissions for fitness data
|
||||||
|
Future<bool> requestPermissions() async {
|
||||||
|
if (!isPlatformSupported) {
|
||||||
|
talker.warning('[Fitness] Platform not supported for fitness data');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final permissions = [
|
||||||
|
HealthDataType.WORKOUT,
|
||||||
|
HealthDataType.STEPS,
|
||||||
|
HealthDataType.DISTANCE_WALKING_RUNNING,
|
||||||
|
HealthDataType.BASAL_ENERGY_BURNED,
|
||||||
|
HealthDataType.ACTIVE_ENERGY_BURNED,
|
||||||
|
];
|
||||||
|
|
||||||
|
final granted = await _health.requestAuthorization(permissions);
|
||||||
|
talker.info('[Fitness] Permission request result: $granted');
|
||||||
|
return granted;
|
||||||
|
} catch (e) {
|
||||||
|
talker.error('[Fitness] Error requesting permissions: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if permissions are granted for fitness data
|
||||||
|
Future<FitnessPermissionStatus> getPermissionStatus() async {
|
||||||
|
if (!isPlatformSupported) {
|
||||||
|
return FitnessPermissionStatus.notDetermined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final permissions = [
|
||||||
|
HealthDataType.WORKOUT,
|
||||||
|
HealthDataType.STEPS,
|
||||||
|
HealthDataType.DISTANCE_WALKING_RUNNING,
|
||||||
|
HealthDataType.BASAL_ENERGY_BURNED,
|
||||||
|
HealthDataType.ACTIVE_ENERGY_BURNED,
|
||||||
|
];
|
||||||
|
|
||||||
|
final granted = await _health.hasPermissions(permissions) ?? true;
|
||||||
|
talker.info('[Fitness] Permission check result: $granted');
|
||||||
|
|
||||||
|
if (granted) {
|
||||||
|
return FitnessPermissionStatus.granted;
|
||||||
|
} else {
|
||||||
|
// Try to check if permissions are denied or restricted
|
||||||
|
try {
|
||||||
|
await _health.requestAuthorization(permissions);
|
||||||
|
return FitnessPermissionStatus.notDetermined;
|
||||||
|
} catch (e) {
|
||||||
|
// If request fails, permissions are likely denied
|
||||||
|
return FitnessPermissionStatus.denied;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
talker.error('[Fitness] Error checking permissions: $e');
|
||||||
|
return FitnessPermissionStatus.notDetermined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get workouts for a specific date range
|
||||||
|
Future<List<FitnessWorkout>> getWorkouts({
|
||||||
|
required DateTime startTime,
|
||||||
|
required DateTime endTime,
|
||||||
|
}) async {
|
||||||
|
if (!isPlatformSupported) {
|
||||||
|
throw Exception('Fitness data is only available on iOS and Android');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final healthData = await _health.getHealthDataFromTypes(
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
|
types: [HealthDataType.WORKOUT],
|
||||||
|
);
|
||||||
|
|
||||||
|
final workouts = <FitnessWorkout>[];
|
||||||
|
|
||||||
|
for (final data in healthData) {
|
||||||
|
if (data.value is WorkoutHealthValue) {
|
||||||
|
final workoutValue = data.value as WorkoutHealthValue;
|
||||||
|
|
||||||
|
final workout = FitnessWorkout(
|
||||||
|
startTime: data.dateFrom,
|
||||||
|
endTime: data.dateTo,
|
||||||
|
workoutType: workoutValue.workoutActivityType,
|
||||||
|
totalEnergyBurned: workoutValue.totalEnergyBurned?.toDouble(),
|
||||||
|
totalEnergyBurnedUnit: workoutValue.totalEnergyBurnedUnit,
|
||||||
|
totalDistance: workoutValue.totalDistance?.toDouble(),
|
||||||
|
totalDistanceUnit: workoutValue.totalDistanceUnit,
|
||||||
|
totalSteps: workoutValue.totalSteps?.toDouble(),
|
||||||
|
totalStepsUnit: workoutValue.totalStepsUnit,
|
||||||
|
);
|
||||||
|
|
||||||
|
workouts.add(workout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by start time (newest first)
|
||||||
|
workouts.sort((a, b) => b.startTime.compareTo(a.startTime));
|
||||||
|
|
||||||
|
talker.info('[Fitness] Retrieved ${workouts.length} workouts');
|
||||||
|
return workouts;
|
||||||
|
} catch (e) {
|
||||||
|
talker.error('[Fitness] Error retrieving workouts: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get workouts from the last 7 days
|
||||||
|
Future<List<FitnessWorkout>> getWorkoutsLast7Days() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final startTime = now.subtract(const Duration(days: 7));
|
||||||
|
return getWorkouts(startTime: startTime, endTime: now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get workouts from the last 30 days
|
||||||
|
Future<List<FitnessWorkout>> getWorkoutsLast30Days() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final startTime = now.subtract(const Duration(days: 30));
|
||||||
|
return getWorkouts(startTime: startTime, endTime: now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get workouts for today
|
||||||
|
Future<List<FitnessWorkout>> getWorkoutsToday() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final tomorrow = today.add(const Duration(days: 1));
|
||||||
|
return getWorkouts(startTime: today, endTime: tomorrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get workouts for this week (Monday to Sunday)
|
||||||
|
Future<List<FitnessWorkout>> getWorkoutsThisWeek() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
// Calculate Monday of current week
|
||||||
|
final daysSinceMonday = now.weekday - 1; // Monday is 1, Sunday is 7
|
||||||
|
final monday = now.subtract(Duration(days: daysSinceMonday));
|
||||||
|
final startOfWeek = DateTime(monday.year, monday.month, monday.day);
|
||||||
|
final endOfWeek = startOfWeek.add(const Duration(days: 7));
|
||||||
|
return getWorkouts(startTime: startOfWeek, endTime: endOfWeek);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get workouts for this month
|
||||||
|
Future<List<FitnessWorkout>> getWorkoutsThisMonth() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final startOfMonth = DateTime(now.year, now.month, 1);
|
||||||
|
final startOfNextMonth = startOfMonth.add(const Duration(days: 32));
|
||||||
|
final endOfMonth = DateTime(
|
||||||
|
startOfNextMonth.year,
|
||||||
|
startOfNextMonth.month,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
return getWorkouts(startTime: startOfMonth, endTime: endOfMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all available workout types from the device
|
||||||
|
Future<Set<HealthWorkoutActivityType>> getAvailableWorkoutTypes({
|
||||||
|
DateTime? startTime,
|
||||||
|
DateTime? endTime,
|
||||||
|
}) async {
|
||||||
|
if (!isPlatformSupported) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final workouts = await getWorkouts(
|
||||||
|
startTime: startTime ?? DateTime(2020, 1, 1),
|
||||||
|
endTime: endTime ?? DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return workouts.map((w) => w.workoutType).toSet();
|
||||||
|
} catch (e) {
|
||||||
|
talker.error('[Fitness] Error getting available workout types: $e');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get summary statistics for workouts
|
||||||
|
Future<FitnessSummary> getWorkoutSummary({
|
||||||
|
required DateTime startTime,
|
||||||
|
required DateTime endTime,
|
||||||
|
}) async {
|
||||||
|
final workouts = await getWorkouts(startTime: startTime, endTime: endTime);
|
||||||
|
|
||||||
|
double totalCalories = 0;
|
||||||
|
double totalDistance = 0;
|
||||||
|
int totalSteps = 0;
|
||||||
|
int totalWorkouts = workouts.length;
|
||||||
|
Duration totalDuration = Duration.zero;
|
||||||
|
|
||||||
|
for (final workout in workouts) {
|
||||||
|
// Calculate total duration
|
||||||
|
totalDuration += workout.endTime.difference(workout.startTime);
|
||||||
|
|
||||||
|
// Add calories (prefer active energy, fallback to basal)
|
||||||
|
if (workout.totalEnergyBurned != null) {
|
||||||
|
totalCalories += workout.totalEnergyBurned!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add distance
|
||||||
|
if (workout.totalDistance != null) {
|
||||||
|
totalDistance += workout.totalDistance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add steps
|
||||||
|
if (workout.totalSteps != null) {
|
||||||
|
totalSteps += workout.totalSteps!.toInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FitnessSummary(
|
||||||
|
totalWorkouts: totalWorkouts,
|
||||||
|
totalDuration: totalDuration,
|
||||||
|
totalCalories: totalCalories,
|
||||||
|
totalDistance: totalDistance,
|
||||||
|
totalSteps: totalSteps,
|
||||||
|
averageDuration: totalWorkouts > 0
|
||||||
|
? Duration(
|
||||||
|
milliseconds: (totalDuration.inMilliseconds / totalWorkouts)
|
||||||
|
.round(),
|
||||||
|
)
|
||||||
|
: Duration.zero,
|
||||||
|
averageCalories: totalWorkouts > 0 ? totalCalories / totalWorkouts : 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get summary for last 7 days
|
||||||
|
Future<FitnessSummary> getWorkoutSummaryLast7Days() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final startTime = now.subtract(const Duration(days: 7));
|
||||||
|
return getWorkoutSummary(startTime: startTime, endTime: now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if fitness data is available
|
||||||
|
Future<bool> isDataAvailable() async {
|
||||||
|
if (!isPlatformSupported) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final permissionStatus = await getPermissionStatus();
|
||||||
|
if (permissionStatus != FitnessPermissionStatus.granted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get a small sample of data to verify it's available
|
||||||
|
final now = DateTime.now();
|
||||||
|
final yesterday = now.subtract(const Duration(days: 1));
|
||||||
|
final workouts = await getWorkouts(startTime: yesterday, endTime: now);
|
||||||
|
|
||||||
|
return workouts.isNotEmpty;
|
||||||
|
} catch (e) {
|
||||||
|
talker.warning('[Fitness] Error checking data availability: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Summary statistics for fitness data
|
||||||
|
class FitnessSummary {
|
||||||
|
final int totalWorkouts;
|
||||||
|
final Duration totalDuration;
|
||||||
|
final double totalCalories;
|
||||||
|
final double totalDistance;
|
||||||
|
final int totalSteps;
|
||||||
|
final Duration averageDuration;
|
||||||
|
final double averageCalories;
|
||||||
|
|
||||||
|
FitnessSummary({
|
||||||
|
required this.totalWorkouts,
|
||||||
|
required this.totalDuration,
|
||||||
|
required this.totalCalories,
|
||||||
|
required this.totalDistance,
|
||||||
|
required this.totalSteps,
|
||||||
|
required this.averageDuration,
|
||||||
|
required this.averageCalories,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Get formatted total duration string
|
||||||
|
String get totalDurationString {
|
||||||
|
final hours = totalDuration.inHours;
|
||||||
|
final minutes = totalDuration.inMinutes.remainder(60);
|
||||||
|
final seconds = totalDuration.inSeconds.remainder(60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return '${hours}h ${minutes}m ${seconds}s';
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return '${minutes}m ${seconds}s';
|
||||||
|
} else {
|
||||||
|
return '${seconds}s';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get formatted average duration string
|
||||||
|
String get averageDurationString {
|
||||||
|
final hours = averageDuration.inHours;
|
||||||
|
final minutes = averageDuration.inMinutes.remainder(60);
|
||||||
|
final seconds = averageDuration.inSeconds.remainder(60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return '${hours}h ${minutes}m ${seconds}s';
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return '${minutes}m ${seconds}s';
|
||||||
|
} else {
|
||||||
|
return '${seconds}s';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get formatted total calories string
|
||||||
|
String get totalCaloriesString {
|
||||||
|
return '${totalCalories.toStringAsFixed(0)} kcal';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get formatted average calories string
|
||||||
|
String get averageCaloriesString {
|
||||||
|
return '${averageCalories.toStringAsFixed(0)} kcal';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get formatted total distance string
|
||||||
|
String get totalDistanceString {
|
||||||
|
if (totalDistance >= 1000) {
|
||||||
|
return '${(totalDistance / 1000).toStringAsFixed(2)} km';
|
||||||
|
} else {
|
||||||
|
return '${totalDistance.toStringAsFixed(0)} m';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get formatted total steps string
|
||||||
|
String get totalStepsString {
|
||||||
|
return totalSteps.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -282,25 +282,11 @@ class AppWrapper extends HookConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle bottom navigation routes properly to prevent navigation bar disappearance
|
|
||||||
// These routes should navigate within the bottom navigation shell
|
|
||||||
final bottomNavRoutes = ['/', '/explore', '/chat', '/realms', '/account'];
|
|
||||||
if (bottomNavRoutes.contains(path)) {
|
|
||||||
// Navigate within the bottom navigation shell using go() to maintain shell context
|
|
||||||
router.go(path);
|
|
||||||
if (!kIsWeb &&
|
|
||||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
|
||||||
windowManager.show();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.queryParameters.isNotEmpty) {
|
if (uri.queryParameters.isNotEmpty) {
|
||||||
path = Uri.parse(
|
path = Uri.parse(
|
||||||
path,
|
path,
|
||||||
).replace(queryParameters: uri.queryParameters).toString();
|
).replace(queryParameters: uri.queryParameters).toString();
|
||||||
}
|
}
|
||||||
// For non-bottom navigation routes, use push() to navigate outside the shell
|
|
||||||
router.push(path);
|
router.push(path);
|
||||||
if (!kIsWeb &&
|
if (!kIsWeb &&
|
||||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/message.dart';
|
import 'package:island/pods/message.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/services/fitness_data.dart';
|
||||||
|
import 'package:island/services/fitness_service.dart';
|
||||||
import 'package:island/services/update_service.dart';
|
import 'package:island/services/update_service.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/network_status_sheet.dart';
|
import 'package:island/widgets/content/network_status_sheet.dart';
|
||||||
@@ -196,6 +198,66 @@ class DebugSheet extends HookConsumerWidget {
|
|||||||
DefaultCacheManager().emptyCache();
|
DefaultCacheManager().emptyCache();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const Divider(height: 8),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.fitness_center),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: const Text('Load Last 7 Days Workouts'),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
final fitnessService = ref.read(fitnessServiceProvider);
|
||||||
|
|
||||||
|
// Check if platform is supported
|
||||||
|
if (!fitnessService.isPlatformSupported) {
|
||||||
|
showErrorAlert('Fitness data is only available on iOS and Android devices.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions first
|
||||||
|
final permissionStatus = await fitnessService.getPermissionStatus();
|
||||||
|
if (permissionStatus != FitnessPermissionStatus.granted) {
|
||||||
|
final granted = await fitnessService.requestPermissions();
|
||||||
|
if (!granted) {
|
||||||
|
showErrorAlert('Permission to access fitness data was denied. Please enable it in your device settings.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get workouts from the last 7 days
|
||||||
|
final workouts = await fitnessService.getWorkoutsLast7Days();
|
||||||
|
|
||||||
|
if (workouts.isEmpty) {
|
||||||
|
showInfoAlert('No workout data found for the last 7 days.', 'No Data');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the workout data for display
|
||||||
|
StringBuffer sb = StringBuffer();
|
||||||
|
for (final workout in workouts) {
|
||||||
|
final dateStr = '${workout.startTime.day}/${workout.startTime.month}';
|
||||||
|
final energyStr = workout.energyBurnedString.isNotEmpty
|
||||||
|
? ' • ${workout.energyBurnedString}'
|
||||||
|
: '';
|
||||||
|
final distanceStr = workout.distanceString.isNotEmpty
|
||||||
|
? ' • ${workout.distanceString}'
|
||||||
|
: '';
|
||||||
|
final stepsStr = workout.stepsString.isNotEmpty
|
||||||
|
? ' • ${workout.stepsString} steps'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
sb.write(
|
||||||
|
'${workout.workoutTypeString} • $dateStr • ${workout.durationString}$energyStr$distanceStr$stepsStr\n',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showInfoAlert(sb.toString(), 'Workout Data Retrieved');
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -201,6 +201,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
carp_serializable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: carp_serializable
|
||||||
|
sha256: f039f8ea22e9437aef13fe7e9743c3761c76d401288dcb702eadd273c3e4dcef
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
cassowary:
|
cassowary:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1269,6 +1277,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.3.4"
|
version: "5.3.4"
|
||||||
|
health:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: health
|
||||||
|
sha256: "320633022fb2423178baa66508001c4ca5aee5806ffa2c913e66488081e9fd47"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.1.4"
|
||||||
highlight:
|
highlight:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 3.5.0+165
|
version: 3.5.0+164
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">3.10.0"
|
sdk: ">3.10.0"
|
||||||
@@ -174,6 +174,7 @@ dependencies:
|
|||||||
video_thumbnail: ^0.5.6
|
video_thumbnail: ^0.5.6
|
||||||
just_audio: ^0.10.5
|
just_audio: ^0.10.5
|
||||||
audio_session: ^0.2.2
|
audio_session: ^0.2.2
|
||||||
|
health: ^13.1.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user