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/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"), - )), - ], + ), ); }, ); 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] = []; } 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(), diff --git a/lib/presentation/onboarding/providers/permissions_provider.dart b/lib/presentation/onboarding/providers/permissions_provider.dart index efb02a8..5596413 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 + // Check the availability of historical permissions + // This is only relevant for Android, on iOS this is not available + + 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), ),