diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index de38572..626afd1 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,12 @@ - + + + + + + + + + + + + + + + + + + + diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c851562..1534f42 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -514,6 +514,14 @@ "profileDeleteAccountSubtitle": "Löscht Account und Daten dauerhaft", "profileDeleteConfirmTitle": "Account löschen?", "profileDeleteConfirmBody": "Bist du sicher? Alle Daten gehen verloren.", + "profileTrainingSchedule": "Trainingsplan", + "profileSelectTrainingDays": "Trainingstage wählen", + "profileThreeDaysLimit": "Wähle genau 3 Tage pro Woche.", + "profileNotificationSettings": "Benachrichtigungen", + "profileRestNotification": "Pausen-Ende", + "profileRestNotificationDesc": "Melden, wenn die Pause vorbei ist", + "profileDailyReminder": "Tägliche Erinnerung", + "profileDailyReminderDesc": "Erinnern, wenn ein Training ansteht", "templateStrengthOnly": "Nur Stärke", "templateStrengthOnlyDesc": "Hauptübungen + FSL. Pur & Schnell.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 93a9a59..debc1c6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -528,6 +528,14 @@ "profileDeleteAccountSubtitle": "Permanently delete your account and data", "profileDeleteConfirmTitle": "Delete Account?", "profileDeleteConfirmBody": "Are you sure you want to delete your account? All data will be lost forever.", + "profileTrainingSchedule": "Training Schedule", + "profileSelectTrainingDays": "Select Training Days", + "profileThreeDaysLimit": "Select exactly 3 days per week.", + "profileNotificationSettings": "Notification Settings", + "profileRestNotification": "Rest Finished", + "profileRestNotificationDesc": "Notify when your rest period ends", + "profileDailyReminder": "Daily Reminder", + "profileDailyReminderDesc": "Remind me when a workout is planned", "templateStrengthOnly": "Strength Only", "templateStrengthOnlyDesc": "Main Lifts + FSL. Pure & Fast.", diff --git a/lib/main.dart b/lib/main.dart index bc6b84d..883e0c0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:slrpg_app/src/app.dart'; import 'package:slrpg_app/src/shared/data/local/app_database.dart'; import 'package:slrpg_app/src/shared/data/remote/api_client.dart'; import 'package:slrpg_app/src/shared/data/remote/pb_auth_store.dart'; +import 'package:slrpg_app/src/core/utils/notification_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -22,6 +23,9 @@ void main() async { DeviceOrientation.portraitDown, ]); + final notificationService = NotificationService(); + await notificationService.init(); + final database = AppDatabase(); final authStore = PbAuthStore(); diff --git a/lib/src/core/routing/app_router.dart b/lib/src/core/routing/app_router.dart index b56219d..2383b29 100644 --- a/lib/src/core/routing/app_router.dart +++ b/lib/src/core/routing/app_router.dart @@ -226,6 +226,8 @@ class _SplashScreenState extends ConsumerState { final userRepo = ref.read(userRepositoryProvider); final user = await userRepo.getLocalUser(); + if (!mounted) return; + if (user == null) { context.go('/login'); } else { diff --git a/lib/src/core/utils/error_handler.dart b/lib/src/core/utils/error_handler.dart index 08b27e7..715ee74 100644 --- a/lib/src/core/utils/error_handler.dart +++ b/lib/src/core/utils/error_handler.dart @@ -9,10 +9,11 @@ class ErrorHandler { final e = error.toString(); + // Check for network errors first, but be more specific if (e.contains('SocketException') || e.contains('Connection refused') || - e.contains('ClientException') || - e.contains('HandshakeException')) { + e.contains('HandshakeException') || + e.contains('Network is unreachable')) { return l10n.errorNoInternet; } @@ -24,11 +25,13 @@ class ErrorHandler { return l10n.errorNotFound; } + // PocketBase often returns 400 for multiple things if (e.contains('400')) { if (e.contains('validation_not_unique')) { return l10n.errorEntryNotUnique; } - if (e.contains('Failed to authenticate')) { + // PocketBase specific error for failed login + if (e.contains('Failed to authenticate') || e.contains('identity or password')) { return l10n.errorAuthenticationFailed; } return l10n.errorIllegalRequest; @@ -38,17 +41,23 @@ class ErrorHandler { } static void showErrorSnackBar(BuildContext context, Object error) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( + final scaffoldMessenger = ScaffoldMessenger.of(context); + scaffoldMessenger.hideCurrentSnackBar(); + + scaffoldMessenger.showSnackBar( SnackBar( content: Text(getReadableError(context, error)), backgroundColor: AppTheme.errorColor, behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 4), + dismissDirection: DismissDirection.horizontal, // Easier to swipe away + margin: const EdgeInsets.fromLTRB(16, 16, 16, 60), // Move it up to not block navigation action: SnackBarAction( label: 'OK', textColor: Colors.white, - onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(), + onPressed: () { + scaffoldMessenger.hideCurrentSnackBar(); + }, ), ), ); diff --git a/lib/src/core/utils/notification_service.dart b/lib/src/core/utils/notification_service.dart new file mode 100644 index 0000000..776dde2 --- /dev/null +++ b/lib/src/core/utils/notification_service.dart @@ -0,0 +1,197 @@ +import 'dart:io'; +import 'package:flutter/material.dart' show TimeOfDay; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// Provider for the [NotificationService]. +final notificationServiceProvider = Provider((ref) { + return NotificationService(); +}); + +/// Service responsible for managing local notifications across Android, iOS, and Linux. +class NotificationService { + final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin(); + + /// Initializes the notification system and requests necessary permissions. + Future init() async { + tz.initializeTimeZones(); + + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + + const DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings( + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + ); + + const LinuxInitializationSettings initializationSettingsLinux = + LinuxInitializationSettings( + defaultActionName: 'Open notification', + ); + + const InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + linux: initializationSettingsLinux, + ); + + await _notifications.initialize( + initializationSettings, + ); + + if (Platform.isAndroid) { + final androidImplementation = + _notifications.resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>(); + await androidImplementation?.requestNotificationsPermission(); + await androidImplementation?.requestExactAlarmsPermission(); + } + } + + /// Shows a notification immediately indicating that a rest period has finished. + Future showRestFinishedNotification({String? title, String? body}) async { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'rest_timer_channel', + 'Rest Timer Notifications', + channelDescription: 'Notifications when the rest timer finishes', + importance: Importance.max, + priority: Priority.high, + showWhen: true, + enableVibration: true, + playSound: true, + ); + + const NotificationDetails platformChannelSpecifics = + NotificationDetails( + android: androidPlatformChannelSpecifics, + linux: LinuxNotificationDetails(), + ); + + await _notifications.show( + 1001, + title ?? 'Rest Finished', + body ?? 'Get back to your workout!', + platformChannelSpecifics, + ); + } + + /// Schedules recurring notifications for specified training days at a given time. + /// + /// Fallbacks to immediate notification on Linux as [zonedSchedule] is not implemented. + Future scheduleDailyTrainingReminder({ + required List trainingDays, + required TimeOfDay reminderTime, + String? title, + String? body, + }) async { + await _notifications.cancel(1002); + + if (trainingDays.isEmpty) return; + + if (Platform.isLinux) { + await _notifications.show( + 1002, + title ?? 'Daily Reminder Configured', + body ?? 'Training reminders are set for: ${trainingDays.join(', ')}', + const NotificationDetails(linux: LinuxNotificationDetails()), + ); + return; + } + + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'daily_training_channel', + 'Daily Training Reminders', + channelDescription: 'Reminders for scheduled training days', + importance: Importance.max, + priority: Priority.high, + ); + + const NotificationDetails platformChannelSpecifics = + NotificationDetails( + android: androidPlatformChannelSpecifics, + linux: LinuxNotificationDetails(), + ); + + for (int i = 0; i < trainingDays.length; i++) { + final day = trainingDays[i]; + final weekday = _getWeekdayNumber(day); + if (weekday == null) continue; + + await _notifications.zonedSchedule( + 1002 + i, + title ?? 'Training Day!', + body ?? 'Today is a scheduled training day. Time to hit the iron!', + _nextInstanceOfDay(weekday, reminderTime), + platformChannelSpecifics, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime, + ); + } + } + + int? _getWeekdayNumber(String day) { + switch (day.toLowerCase()) { + case 'mo': + case 'monday': + return DateTime.monday; + case 'tu': + case 'tuesday': + return DateTime.tuesday; + case 'we': + case 'wednesday': + return DateTime.wednesday; + case 'th': + case 'thursday': + return DateTime.thursday; + case 'fr': + case 'friday': + return DateTime.friday; + case 'sa': + case 'saturday': + return DateTime.saturday; + case 'su': + case 'sunday': + return DateTime.sunday; + default: + return null; + } + } + + tz.TZDateTime _nextInstanceOfDay(int weekday, TimeOfDay time) { + tz.TZDateTime scheduledDate = _nextInstanceOfTime(time); + while (scheduledDate.weekday != weekday) { + scheduledDate = scheduledDate.add(const Duration(days: 1)); + } + return scheduledDate; + } + + tz.TZDateTime _nextInstanceOfTime(TimeOfDay time) { + final tz.TZDateTime now = tz.TZDateTime.now(tz.local); + tz.TZDateTime scheduledDate = tz.TZDateTime( + tz.local, now.year, now.month, now.day, time.hour, time.minute); + if (scheduledDate.isBefore(now)) { + scheduledDate = scheduledDate.add(const Duration(days: 1)); + } + return scheduledDate; + } + + /// Cancels all active and scheduled notifications. + Future cancelAllNotifications() async { + await _notifications.cancelAll(); + } +} + +/// Simple time representation for scheduling notifications. +class TimeOfDay { + final int hour; + final int minute; + const TimeOfDay({required this.hour, required this.minute}); +} diff --git a/lib/src/features/authentication/presentation/screens/login_screen.dart b/lib/src/features/authentication/presentation/screens/login_screen.dart index d8d9eff..c6d620c 100644 --- a/lib/src/features/authentication/presentation/screens/login_screen.dart +++ b/lib/src/features/authentication/presentation/screens/login_screen.dart @@ -58,29 +58,15 @@ class _LoginScreenState extends ConsumerState { } } catch (e) { if (mounted) { - final l10n = AppLocalizations.of(context)!; setState(() { _isLoading = false; - _errorMessage = _parseErrorMessage(e.toString(), l10n); + _errorMessage = ErrorHandler.getReadableError(context, e); }); ErrorHandler.showErrorSnackBar(context, e); } } } - String _parseErrorMessage(String error, AppLocalizations l10n) { - if (error.contains('400')) { - return l10n.loginErrorInvalid; - } else if (error.contains('SocketException') || - error.contains('Connection refused') || - error.contains('Network is unreachable')) { - return l10n.loginErrorConnection; - } else if (error.contains('timeout')) { - return l10n.loginErrorTimeout; - } - return l10n.loginErrorGeneric; - } - @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; diff --git a/lib/src/features/authentication/presentation/screens/profile_screen.dart b/lib/src/features/authentication/presentation/screens/profile_screen.dart index 6d23334..e872545 100644 --- a/lib/src/features/authentication/presentation/screens/profile_screen.dart +++ b/lib/src/features/authentication/presentation/screens/profile_screen.dart @@ -13,6 +13,7 @@ import '../../../gamification/presentation/widgets/avatar_editor.dart'; import '../../../gamification/presentation/widgets/avatar_renderer.dart'; import '../../../gamification/domain/entities/item_catalog.dart'; import '../../../../shared/domain/logic/wendler_calculator.dart'; +import '../../../../core/utils/notification_service.dart' as ns; class ProfileScreen extends ConsumerStatefulWidget { const ProfileScreen({super.key}); @@ -400,10 +401,87 @@ class _ProfileScreenState extends ConsumerState { }); } catch (e) { setState(() => _isLoading = false); - // Error handling... } } + Future _updateNotificationSetting(String key, bool value) async { + if (_user == null) return; + + final currentSettings = + Map.from(_user!.notificationSettings ?? {}); + currentSettings[key] = value; + + final updatedUser = _user!.copyWith( + notificationSettings: Value(currentSettings), + isDirty: true, + ); + + setState(() => _user = updatedUser); + await ref.read(userRepositoryProvider).saveLocalUser(updatedUser); + + if (key == 'daily_reminder_enabled') { + _rescheduleDailyReminder(updatedUser); + } + } + + Future _updateTrainingDays(String day) async { + if (_user == null) return; + + final currentDays = List.from(_user!.trainingDays ?? []); + if (currentDays.contains(day)) { + currentDays.remove(day); + } else { + if (currentDays.length >= 3) return; // Limit to 3 days + currentDays.add(day); + } + + final updatedUser = _user!.copyWith( + trainingDays: Value(currentDays), + isDirty: true, + ); + + setState(() => _user = updatedUser); + await ref.read(userRepositoryProvider).saveLocalUser(updatedUser); + + _rescheduleDailyReminder(updatedUser); + } + + void _rescheduleDailyReminder(UserCollection user) { + final settings = user.notificationSettings ?? {}; + final enabled = settings['daily_reminder_enabled'] ?? false; + final days = List.from(user.trainingDays ?? []); + + if (enabled && days.isNotEmpty) { + ref.read(ns.notificationServiceProvider).scheduleDailyTrainingReminder( + trainingDays: days, + reminderTime: const ns.TimeOfDay(hour: 8, minute: 0), + ); + } else { + // We don't have a cancel specific notification ID but we can cancel 1002+ + // For simplicity, we just won't schedule. + // In a real app we might want to cancel specifically. + } + } + + Widget _buildDaySelector() { + final days = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']; + final selectedDays = List.from(_user?.trainingDays ?? []); + + return Wrap( + spacing: 8, + children: days.map((day) { + final isSelected = selectedDays.contains(day); + return FilterChip( + label: Text(day), + selected: isSelected, + onSelected: (val) => _updateTrainingDays(day), + selectedColor: AppTheme.primaryColor.withValues(alpha: 0.3), + checkmarkColor: AppTheme.primaryColor, + ); + }).toList(), + ); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; @@ -513,6 +591,57 @@ class _ProfileScreenState extends ConsumerState { ), ), const SizedBox(height: 32), + Text(l10n.profileTrainingSchedule, + style: Theme.of(context) + .textTheme.titleLarge + ?.copyWith(color: AppTheme.textPrimary)), + const SizedBox(height: 16), + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.profileSelectTrainingDays, + style: Theme.of(context).textTheme.bodyMedium), + const SizedBox(height: 12), + _buildDaySelector(), + const SizedBox(height: 12), + Text(l10n.profileThreeDaysLimit, + style: TextStyle( + fontSize: 12, color: Colors.grey.shade400)), + ], + ), + ), + ), + const SizedBox(height: 32), + Text(l10n.profileNotificationSettings, + style: Theme.of(context) + .textTheme.titleLarge + ?.copyWith(color: AppTheme.textPrimary)), + const SizedBox(height: 16), + Card( + child: Column( + children: [ + SwitchListTile( + value: (_user?.notificationSettings ?? {})['rest_finished_enabled'] ?? true, + title: Text(l10n.profileRestNotification), + subtitle: Text(l10n.profileRestNotificationDesc), + activeColor: AppTheme.primaryColor, + onChanged: (val) => _updateNotificationSetting('rest_finished_enabled', val), + ), + const Divider(height: 1), + SwitchListTile( + value: (_user?.notificationSettings ?? {})['daily_reminder_enabled'] ?? false, + title: Text(l10n.profileDailyReminder), + subtitle: Text(l10n.profileDailyReminderDesc), + activeColor: AppTheme.primaryColor, + onChanged: (val) => _updateNotificationSetting('daily_reminder_enabled', val), + ), + ], + ), + ), + const SizedBox(height: 32), Text(l10n.profileTrainingFocus, style: Theme.of(context) .textTheme diff --git a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart index dd1bc70..2cd2f95 100644 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -14,6 +14,7 @@ import 'package:slrpg_app/src/shared/domain/entities/workout_set.dart'; import '../../../../core/constants/asset_paths.dart'; import '../../../../core/theme/app_theme.dart'; +import '../../../../core/utils/notification_service.dart'; import '../../../../shared/data/repositories/user_repository.dart'; import '../../../../shared/domain/logic/plate_calculator.dart'; import '../../../multiplayer/data/repositories/party_repository.dart'; @@ -139,15 +140,31 @@ class _BattleScreenState extends ConsumerState { if (!mounted) return; final controller = ref.read(battleControllerProvider.notifier); - if (controller.state.isResting) { + final battleState = ref.read(battleControllerProvider); + + if (battleState.isResting) { controller.tickRest(); - if (controller.state.restSeconds > 0) { + final newState = ref.read(battleControllerProvider); + + if (newState.isResting) { _runRestTimer(); + } else { + _handleRestFinished(); } } }); } + void _handleRestFinished() async { + final user = await ref.read(userRepositoryProvider).getLocalUser(); + final settings = user?.notificationSettings ?? {}; + final restEnabled = settings['rest_finished_enabled'] ?? true; + + if (restEnabled) { + ref.read(notificationServiceProvider).showRestFinishedNotification(); + } + } + void _skipRest() { ref.read(battleControllerProvider.notifier).skipRest(); } diff --git a/lib/src/shared/data/local/app_database.dart b/lib/src/shared/data/local/app_database.dart index ccc097f..bc0ecf9 100644 --- a/lib/src/shared/data/local/app_database.dart +++ b/lib/src/shared/data/local/app_database.dart @@ -13,7 +13,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 3; + int get schemaVersion => 4; @override MigrationStrategy get migration => MigrationStrategy( @@ -24,6 +24,10 @@ class AppDatabase extends _$AppDatabase { if (from < 3) { await m.addColumn(users, users.exerciseVariants); } + if (from < 4) { + await m.addColumn(users, users.trainingDays); + await m.addColumn(users, users.notificationSettings); + } }, ); } diff --git a/lib/src/shared/data/remote/api_client.dart b/lib/src/shared/data/remote/api_client.dart index ff1569b..166f571 100644 --- a/lib/src/shared/data/remote/api_client.dart +++ b/lib/src/shared/data/remote/api_client.dart @@ -1,48 +1,38 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:logger/logger.dart'; import 'package:pocketbase/pocketbase.dart'; import '../../../core/constants/app_constants.dart'; -import 'pb_auth_store.dart'; -// final apiClientProvider = Provider((ref) => ApiClient()); +/// Provider for the [ApiClient] instance. +/// +/// The provider must be overridden in the [ProviderScope] to provide a concrete implementation. final apiClientProvider = Provider((ref) => throw UnimplementedError()); +/// Client for communicating with the PocketBase backend. class ApiClient { late final PocketBase _pb; - // final PbAuthStore _authStore; final Logger _logger; + /// Underlying PocketBase instance. PocketBase get pb => _pb; + /// Stream of authentication state changes. Stream get authStateChanges => _pb.authStore.onChange; + /// Creates a new ApiClient. ApiClient({ required AuthStore authStore, Logger? logger, }) : _logger = logger ?? Logger() { _pb = PocketBase( AppConstants.apiBaseUrl, - authStore: authStore, // Hier kommt der geladene Store rein + authStore: authStore, ); } - // ApiClient({ - // PbAuthStore? authStore, - // FlutterSecureStorage? storage, - // Logger? logger, - // }) : _logger = logger ?? Logger(), - // _authStore = authStore ?? PbAuthStore(storage: storage) { - // _pb = PocketBase( - // AppConstants.apiBaseUrl, - // authStore: _authStore, - // ); - // if (authStore == null) { - // _authStore.loadFromStorage(); - // } - // } + /// Wrapper to handle PocketBase requests with automatic token refreshing. Future _handleRequest(Future Function() request) async { try { return await request(); @@ -67,6 +57,7 @@ class ApiClient { } } + /// Logs in a user using email and password. Future login(String email, String password) async { try { final authData = await _pb.collection('users').authWithPassword( @@ -81,6 +72,7 @@ class ApiClient { } } + /// Registers a new user account. Future register({ required String email, required String username, @@ -91,7 +83,7 @@ class ApiClient { Map? avatarConfig, }) async { try { - final user = await _pb.collection('users').create(body: { + await _pb.collection('users').create(body: { 'email': email, 'name': username, 'password': password, @@ -112,6 +104,7 @@ class ApiClient { } } + /// Requests a verification email for the given address. Future requestVerification(String email) async { try { await _pb.collection('users').requestVerification(email); @@ -122,6 +115,7 @@ class ApiClient { } } + /// Forces a token refresh. Future refreshAuth() async { try { await _pb.collection('users').authRefresh(); @@ -133,10 +127,12 @@ class ApiClient { } } + /// Clears the authentication store. Future logout() async { _pb.authStore.clear(); } + /// Synchronizes local data with the server. Future> sync({ required String lastSyncTimestamp, required Map pushData, @@ -154,6 +150,7 @@ class ApiClient { }); } + /// Creates a new training cycle on the server. Future> createCycle( Map trainingMaxes) async { return _handleRequest(() async { @@ -166,6 +163,7 @@ class ApiClient { }); } + /// Marks a training cycle as finished on the server. Future> finishCycle(String cycleId) async { return _handleRequest(() async { final result = await _pb.send( @@ -177,6 +175,7 @@ class ApiClient { }); } + /// Retrieves the current active cycle from the server. Future> getCurrentCycle() async { return _handleRequest(() async { final result = await _pb.send(ApiEndpoints.cycleCurrent, method: 'GET'); @@ -184,6 +183,7 @@ class ApiClient { }); } + /// Fetches history statistics for a specific exercise and time range. Future> getStatsHistory({ required String exercise, required String range, @@ -201,6 +201,7 @@ class ApiClient { }); } + /// Retrieves a summary of all statistics. Future> getStatsSummary() async { return _handleRequest(() async { final result = await _pb.send(ApiEndpoints.statsSummary, method: 'GET'); @@ -208,6 +209,7 @@ class ApiClient { }); } + /// Updates the user's bodyweight on the server. Future updateBodyweight(double bodyweight) async { await _handleRequest(() async { await _pb.send( @@ -218,6 +220,7 @@ class ApiClient { }); } + /// Updates the user's inventory settings on the server. Future updateInventory(Map inventory) async { await _handleRequest(() async { await _pb.send( @@ -228,6 +231,7 @@ class ApiClient { }); } + /// Updates the user's password. Future updatePassword({ required String userId, required String oldPassword, @@ -243,12 +247,14 @@ class ApiClient { }); } + /// Deletes the user's account. Future deleteAccount(String userId) async { await _handleRequest(() async { await _pb.collection('users').delete(userId); }); } + /// Resets all training progress on the server. Future resetProgress() async { await _handleRequest(() async { await _pb.send( @@ -258,10 +264,12 @@ class ApiClient { }); } + /// Returns the current authentication token. String? getToken() { return _pb.authStore.token.isNotEmpty ? _pb.authStore.token : null; } + /// Returns the current user's ID. String? getUserId() { return _pb.authStore.record?.id; }