From e8fa2b574fc4945a3f60a99936ff7d412cff6be9 Mon Sep 17 00:00:00 2001 From: Jozys Date: Sun, 27 Apr 2025 17:27:40 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=90=9B=20Fix=20local=20time=20convers?= =?UTF-8?q?ion=20for=20workout=20start=20date?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activities/list/view_model/activities_view_model.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/presentation/activities/list/view_model/activities_view_model.dart b/lib/presentation/activities/list/view_model/activities_view_model.dart index ab7e222..7c690d2 100644 --- a/lib/presentation/activities/list/view_model/activities_view_model.dart +++ b/lib/presentation/activities/list/view_model/activities_view_model.dart @@ -53,6 +53,7 @@ class ActivitiesViewModel extends StateNotifier { for (int i = 0; i < workouts.length; i++) { ActivityPreview workout = workouts[i]; + state.icons ??= {}; if (state.icons!.containsKey(workout.sourceId)) { workouts[i].icon = state.icons?[workout.sourceId]; @@ -63,8 +64,10 @@ class ActivitiesViewModel extends StateNotifier { state.icons![workout.sourceId] = workouts[i].icon; state = state.copyWith(newIcons: state.icons); } + // Convert the start date to local time + final startDateLocal = toLocal(workout.start); DateTime day = DateTime( - workout.start.year, workout.start.month, workout.start.day); + startDateLocal.year, startDateLocal.month, startDateLocal.day); if (workoutsByDay[day] == null) { workoutsByDay[day] = []; } From f836097c1f67bb4db9fa09e6dcc9642a48297a1b Mon Sep 17 00:00:00 2001 From: Jozys Date: Sun, 27 Apr 2025 17:37:25 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20ListView=20?= =?UTF-8?q?in=20Activities=20for=20better=20refresh=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../list/screen/activities_screen.dart | 103 ++++++++++-------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/lib/presentation/activities/list/screen/activities_screen.dart b/lib/presentation/activities/list/screen/activities_screen.dart index a436e8e..430778c 100644 --- a/lib/presentation/activities/list/screen/activities_screen.dart +++ b/lib/presentation/activities/list/screen/activities_screen.dart @@ -1,4 +1,5 @@ import 'dart:io' show Platform; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -141,51 +142,67 @@ Widget _buildGroupedActivities( return seconds ~/ 60; } - return ListView.builder( - controller: scrollController, - physics: const ScrollPhysics(), - itemCount: activities?.length ?? 0, - itemBuilder: (context, index) { - final date = activities?.keys.elementAt(index); - final activityList = activities?[date]! ?? []; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (date != null && activityList.isNotEmpty) - Padding( - padding: - const EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - DateFormat("dd. MMMM yyyy").format(date), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Text( - getDuration(getActivityMinutes(activityList), context), - style: const TextStyle(fontWeight: FontWeight.bold), + return LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + controller: scrollController, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: activities?.length ?? 0, + itemBuilder: (context, index) { + final date = activities?.keys.elementAt(index); + final activityList = activities?[date]! ?? []; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (date != null && activityList.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + top: 16.0, left: 16.0, right: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + DateFormat("dd. MMMM yyyy").format(date), + style: + const TextStyle(fontWeight: FontWeight.bold), + ), + Text( + getDuration( + getActivityMinutes(activityList), context), + style: + const TextStyle(fontWeight: FontWeight.bold), + ), + ]), ), - ]), - ), - const Divider(), - ...activityList.map( - (activity) => _buildActivityItem(context, activity), + const Divider(), + ...activityList.map( + (activity) => _buildActivityItem(context, activity), + ), + // Add a loading indicator at the end of the list, + if (index == activities!.length - 1 && isLoading) + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Center(child: CircularProgressIndicator()), + ) // Show loading + // Show no more entries text if end is reached + else if (index == activities.length - 1 && !isLoading) + const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: Text("No more entries"), + )), + ], + ); + }, ), - // Add a loading indicator at the end of the list, - if (index == activities!.length - 1 && isLoading) - const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Center(child: CircularProgressIndicator()), - ) // Show loading - // Show no more entries text if end is reached - else if (index == activities.length - 1 && !isLoading) - const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: Text("No more entries"), - )), - ], + ), ); }, ); From fc5e77886240da6d100c41201802bb81ee9aa6a8 Mon Sep 17 00:00:00 2001 From: jozys Date: Mon, 28 Apr 2025 15:41:27 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Center=20tab=20alignme?= =?UTF-8?q?nt=20in=20badge=20lists=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../challenges/badgeLists/screen/badge_lists_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/presentation/challenges/badgeLists/screen/badge_lists_screen.dart b/lib/presentation/challenges/badgeLists/screen/badge_lists_screen.dart index a452374..9482a75 100644 --- a/lib/presentation/challenges/badgeLists/screen/badge_lists_screen.dart +++ b/lib/presentation/challenges/badgeLists/screen/badge_lists_screen.dart @@ -138,6 +138,7 @@ class _BadgeTabsView extends StatelessWidget { children: [ TabBar( isScrollable: true, + tabAlignment: TabAlignment.center, tabs: categories .map((category) => Tab(text: _getCategoryName(category, l10n))) .toList(), From f4ba43da856303b53237157b5a184ad4bf7410fb Mon Sep 17 00:00:00 2001 From: jozys Date: Mon, 28 Apr 2025 17:19:02 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Check=20whether=20hist?= =?UTF-8?q?orical=20health=20functionality=20is=20available?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - In case of Health Connect historical health functionality is not available, hide the corresponding PermissionCard - Add localization for missing read permission --- lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + .../providers/permissions_provider.dart | 25 ++++++++++++++++++- .../onboarding/widgets/permissions_page.dart | 10 +++----- .../screen/permissions_settings_screen.dart | 10 +++----- 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c565d3c..caf29b9 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -338,6 +338,7 @@ "permission_health_read_description": "Zum Lesen deiner Aktivitäten und Fitness-Daten", "permission_health_write_title": "Gesundheitsdaten Schreiben", "permission_health_write_description": "Zum Speichern deiner Aktivitäten in der Health-Datenbank", + "permission_health_read_missing": "Bitte erteile zuerst die Leseberechtigung für Gesundheitsdaten", "permission_health_historical_title": "Historische Gesundheitsdaten", "permission_health_historical_description": "Für den Zugriff auf Gesundheitsdaten, die älter als 30 Tage sind, für Langzeitanalysen", "permission_allow": "Erlauben", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d8d3b43..2a83bc2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -357,6 +357,7 @@ "permission_health_read_description": "To read your activities and fitness data", "permission_health_write_title": "Health Data Write", "permission_health_write_description": "To save your activities to the health database", + "permission_health_read_missing": "Please first allow access to read your health data", "permission_health_historical_title": "Historical Health Data", "permission_health_historical_description": "To access your health data older than 30 days for long-term analysis", "permission_allow": "Allow", diff --git a/lib/presentation/onboarding/providers/permissions_provider.dart b/lib/presentation/onboarding/providers/permissions_provider.dart index efb02a8..1b4baea 100644 --- a/lib/presentation/onboarding/providers/permissions_provider.dart +++ b/lib/presentation/onboarding/providers/permissions_provider.dart @@ -19,6 +19,12 @@ class PermissionsState { final bool healthWritePermissionStatus; final bool healthHistoricalPermissionStatus; + /* + * Determine if historical health functionality is available + * Only relevant for Android, on iOS this is always false + * */ + final bool isHealthHistoricalAvailable; + bool get hasRequiredPermissions => healthPermissionStatus; PermissionsState({ @@ -28,6 +34,7 @@ class PermissionsState { this.healthPermissionStatus = false, this.healthWritePermissionStatus = false, this.healthHistoricalPermissionStatus = false, + this.isHealthHistoricalAvailable = false, }); PermissionsState copyWith({ @@ -37,6 +44,7 @@ class PermissionsState { bool? healthPermissionStatus, bool? healthWritePermissionStatus, bool? healthHistoricalPermissionStatus, + bool? isHealthHistoricalAvailable, }) { return PermissionsState( locationPermissionStatus: @@ -51,6 +59,8 @@ class PermissionsState { healthWritePermissionStatus ?? this.healthWritePermissionStatus, healthHistoricalPermissionStatus: healthHistoricalPermissionStatus ?? this.healthHistoricalPermissionStatus, + isHealthHistoricalAvailable: + isHealthHistoricalAvailable ?? this.isHealthHistoricalAvailable, ); } @@ -284,7 +294,20 @@ class PermissionsNotifier extends StateNotifier { } try { - // Verwende die direkte API-Methode, um zu prüfen, ob historische Berechtigungen gewährt wurden + // Überprüfe die Verfügbarkeit historischer Berechtigungen + // Dies ist nur für Android relevant, auf iOS ist dies nicht verfügbar + + bool isHistoricalAvailable = + await _health.isHealthDataHistoryAvailable(); + state = + state.copyWith(isHealthHistoricalAvailable: isHistoricalAvailable); + if (!isHistoricalAvailable) { + log.warning("Historical Health feature is not available"); + return; + } + + // Verwende die direkte API-Methode, um zu prüfen, ob historische Berechtigungen gewährt wurde + bool hasHistoricalPermissions = await _health.isHealthDataHistoryAuthorized(); diff --git a/lib/presentation/onboarding/widgets/permissions_page.dart b/lib/presentation/onboarding/widgets/permissions_page.dart index 2cc987b..42ff2b3 100644 --- a/lib/presentation/onboarding/widgets/permissions_page.dart +++ b/lib/presentation/onboarding/widgets/permissions_page.dart @@ -144,8 +144,7 @@ class PermissionsPage extends ConsumerWidget { if (!permissionsState.healthPermissionStatus) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - 'Bitte erteile zuerst die Leseberechtigung für Gesundheitsdaten'), + content: Text(l10n.permission_health_read_missing), backgroundColor: theme.colorScheme.error, duration: const Duration(seconds: 2), ), @@ -200,8 +199,8 @@ class PermissionsPage extends ConsumerWidget { const SizedBox(height: 16), - // Historical Health Data permission - only for Android - if (Theme.of(context).platform == TargetPlatform.android) + // Historical Health Data permission - only if available + if (permissionsState.isHealthHistoricalAvailable) Column( children: [ PermissionCard( @@ -216,8 +215,7 @@ class PermissionsPage extends ConsumerWidget { if (!permissionsState.healthPermissionStatus) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: const Text( - 'Bitte erteile zuerst die Leseberechtigung für Gesundheitsdaten'), + content: Text(l10n.permission_health_read_missing), backgroundColor: theme.colorScheme.error, duration: const Duration(seconds: 2), ), diff --git a/lib/presentation/profile/screen/permissions_settings_screen.dart b/lib/presentation/profile/screen/permissions_settings_screen.dart index 59520a0..d69a258 100644 --- a/lib/presentation/profile/screen/permissions_settings_screen.dart +++ b/lib/presentation/profile/screen/permissions_settings_screen.dart @@ -136,8 +136,7 @@ class PermissionsSettingsScreen extends HookConsumerWidget { if (!permissionsState.healthPermissionStatus) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - 'Bitte erteile zuerst die Leseberechtigung für Gesundheitsdaten'), + content: Text(l10n.permission_health_read_missing), backgroundColor: theme.colorScheme.error, duration: const Duration(seconds: 2), ), @@ -193,8 +192,8 @@ class PermissionsSettingsScreen extends HookConsumerWidget { const SizedBox(height: 16), - // Historical Health Data permission - only for Android - if (Theme.of(context).platform == TargetPlatform.android) + // Historical Health Data permission - only if available + if (permissionsState.isHealthHistoricalAvailable) PermissionCard( icon: Icons.history, title: l10n.permission_health_historical_title, @@ -207,8 +206,7 @@ class PermissionsSettingsScreen extends HookConsumerWidget { if (!permissionsState.healthPermissionStatus) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - 'Bitte erteile zuerst die Leseberechtigung für Gesundheitsdaten'), + content: Text(l10n.permission_health_read_missing), backgroundColor: theme.colorScheme.error, duration: const Duration(seconds: 2), ), From 7f0d217701be9df42c70b52e6c82596149da1162 Mon Sep 17 00:00:00 2001 From: jozys Date: Tue, 29 Apr 2025 12:26:23 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20comments=20fo?= =?UTF-8?q?r=20clarity=20on=20historical=20permissions=20availability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onboarding/providers/permissions_provider.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/onboarding/providers/permissions_provider.dart b/lib/presentation/onboarding/providers/permissions_provider.dart index 1b4baea..5596413 100644 --- a/lib/presentation/onboarding/providers/permissions_provider.dart +++ b/lib/presentation/onboarding/providers/permissions_provider.dart @@ -294,8 +294,8 @@ class PermissionsNotifier extends StateNotifier { } try { - // Überprüfe die Verfügbarkeit historischer Berechtigungen - // Dies ist nur für Android relevant, auf iOS ist dies nicht verfügbar + // Check the availability of historical permissions + // This is only relevant for Android, on iOS this is not available bool isHistoricalAvailable = await _health.isHealthDataHistoryAvailable();