From 1e573904f274c428fde5a0fe7e99ecfd1913c4cf Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Wed, 21 Jan 2026 09:00:24 +0100 Subject: [PATCH] feat: add privacy policy to register and profile screen --- lib/l10n/app_de.arb | 53 ++- lib/l10n/app_en.arb | 67 +++- lib/main.dart | 10 +- lib/src/app.dart | 1 - lib/src/core/constants/app_constants.dart | 21 +- lib/src/core/routing/app_router.dart | 7 + .../presentation/screens/profile_screen.dart | 14 +- .../presentation/screens/register_screen.dart | 43 ++- .../backup/domain/backup_service.dart | 44 +++ .../domain/backup_service_provider.dart | 15 + .../presentation/screens/hub_screen.dart | 12 +- .../application/quest_service.dart | 12 +- .../presentation/widgets/quest_item.dart | 1 - .../presentation/screens/history_screen.dart | 4 +- .../repositories/leaderboard_repository.dart | 1 - .../data/repositories/party_repository.dart | 4 +- .../screens/leaderboard_screen.dart | 2 - .../presentation/screens/lobby_screen.dart | 1 - .../screens/avatar_setup_screen.dart | 1 - .../screens/inventory_setup_screen.dart | 13 +- .../screens/privacy_policy_screen.dart | 312 ++++++++++++++++++ .../presentation/screens/stats_screen.dart | 5 +- .../application/battle_state.dart | 118 +++++++ .../workout_generator_service.dart | 19 +- .../presentation/screens/battle_screen.dart | 6 +- .../widgets/emom_timer_widget.dart | 4 +- lib/src/shared/data/remote/sync_service.dart | 22 +- .../data/repositories/cycle_repository.dart | 22 +- .../data/repositories/user_repository.dart | 1 - .../shared/domain/logic/plate_calculator.dart | 2 - .../domain/logic/wendler_calculator.dart | 26 +- .../widgets/consent_checkbox.dart | 57 ++++ 32 files changed, 817 insertions(+), 103 deletions(-) create mode 100644 lib/src/features/backup/domain/backup_service.dart create mode 100644 lib/src/features/backup/domain/backup_service_provider.dart create mode 100644 lib/src/features/settings/presentation/screens/privacy_policy_screen.dart create mode 100644 lib/src/features/workout_runner/application/battle_state.dart create mode 100644 lib/src/shared/presentation/widgets/consent_checkbox.dart diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8c123c1..1eef152 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -310,5 +310,56 @@ "usernameLabel": "Heldenname", "usernameEmptyError": "Bitte wähle einen Heldennamen", - "usernameShortError": "Name zu kurz" + "usernameShortError": "Name zu kurz", + "privacyPolicyTitle": "Datenschutzerklärung", + "privacyPolicySectionResponsible": "Verantwortlicher", + "privacyPolicySectionResponsibleContent": "Dein Name/Firma\nDeine Adresse\nE-Mail: privacy@example.com\nTelefon: +49 123 456789", + + "privacyPolicySectionDataCollected": "Erhobene Daten", + "privacyPolicySectionDataCollectedContent": "• E-Mail-Adresse (zur Account-Verwaltung)\n• Verschlüsseltes Passwort\n• Trainingsdaten (Gewichte, Wiederholungen, Datum)\n• Körpergewicht\n• Avatar-Einstellungen\n• Geräteinformationen (OS-Version, App-Version)", + + "privacyPolicySectionLegalBasis": "Rechtsgrundlage", + "privacyPolicySectionLegalBasisContent": "Art. 6 Abs. 1 lit. b DSGVO - Vertragserfüllung\nDie Verarbeitung deiner Daten ist notwendig, um dir die Funktionen der App bereitzustellen und deinen Trainingsfortschritt zu speichern.", + + "privacyPolicySectionStorageDuration": "Speicherdauer", + "privacyPolicySectionStorageDurationContent": "Deine Daten werden bis zur Löschung deines Accounts gespeichert. Nach der Account-Löschung werden alle deine Daten innerhalb von 30 Tagen dauerhaft von unseren Servern entfernt.", + + "privacyPolicySectionYourRights": "Deine Rechte", + "privacyPolicySectionYourRightsContent": "Du hast folgende Rechte nach DSGVO:\n\n• Recht auf Auskunft über gespeicherte Daten\n• Recht auf Berichtigung falscher Daten\n• Recht auf Löschung (\"Recht auf Vergessenwerden\")\n• Recht auf Datenübertragbarkeit (Export als JSON)\n• Recht auf Widerruf der Einwilligung\n• Beschwerderecht bei einer Aufsichtsbehörde", + + "privacyPolicySectionDataSharing": "Datenweitergabe", + "privacyPolicySectionDataSharingContent": "Deine Daten werden auf unseren Servern in Deutschland gespeichert. Wir geben deine persönlichen Daten nicht an Dritte weiter, außer:\n\n• Wenn gesetzlich vorgeschrieben\n• Mit deiner ausdrücklichen Zustimmung\n• An technische Dienstleister (Hosting) unter strengen Auftragsverarbeitungsverträgen", + + "privacyPolicySectionSecurity": "Datensicherheit", + "privacyPolicySectionSecurityContent": "Wir setzen branchenübliche Sicherheitsmaßnahmen um:\n\n• Ende-zu-Ende-Verschlüsselung für Datenübertragung (TLS/SSL)\n• Verschlüsselte Passwort-Speicherung (bcrypt)\n• Regelmäßige Sicherheitsaudits\n• Sichere Server-Infrastruktur\n• Zugriffskontrollen und Authentifizierung", + + "privacyPolicySectionDeletion": "Account-Löschung", + "privacyPolicySectionDeletionContent": "Du kannst deinen Account jederzeit in den App-Einstellungen löschen. Alle deine persönlichen Daten werden innerhalb von 30 Tagen dauerhaft gelöscht. Trainingsdaten werden nur mit deiner ausdrücklichen Zustimmung für statistische Zwecke anonymisiert aufbewahrt.", + + "privacyPolicySectionContact": "Kontakt & Fragen", + "privacyPolicySectionContactContent": "Bei Fragen zum Datenschutz oder zur Ausübung deiner Rechte kontaktiere uns bitte unter:\n\nprivacy@example.com", + + "privacyPolicySectionUpdates": "Aktualisierungen", + "privacyPolicySectionUpdatesContent": "Wir können diese Datenschutzerklärung von Zeit zu Zeit aktualisieren. Du wirst über wesentliche Änderungen in der App benachrichtigt. Letzte Aktualisierung: Januar 2026", + + "exportMyData": "Meine Daten exportieren", + "deleteMyAccount": "Account löschen", + "privacyPolicyButton": "Datenschutzerklärung", + + "exportDataSuccess": "Daten erfolgreich exportiert", + "exportDataError": "Fehler beim Export: {error}", + + "deleteAccountConfirmTitle": "Account löschen?", + "deleteAccountConfirmBody": "Diese Aktion kann nicht rückgängig gemacht werden. Alle deine Daten werden innerhalb von 30 Tagen dauerhaft gelöscht.\n\nBist du sicher, dass du fortfahren möchtest?", + "deleteAccountFinalWarning": "Tippe \"LÖSCHEN\" zur Bestätigung:", + "deleteAccountSuccess": "Account erfolgreich gelöscht", + "deleteAccountError": "Fehler beim Löschen: {error}", + "deleteAccountConfirmationMismatch": "Bestätigungstext stimmt nicht überein", + + "consentAcceptPrefix": "Ich akzeptiere die ", + "consentPrivacyPolicy": "Datenschutzerklärung", + "consentAnd": " und die ", + "consentTermsOfService": "Nutzungsbedingungen", + "consentRequired": "Du musst die Datenschutzerklärung und Nutzungsbedingungen akzeptieren", + "termsOfServiceTitle": "Nutzungsbedingungen" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9a5df12..19b7e16 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -310,5 +310,70 @@ "usernameLabel": "Hero Name", "usernameEmptyError": "Please choose a hero name", - "usernameShortError": "Name too short" + "usernameShortError": "Name too short", + "privacyPolicyTitle": "Privacy Policy", + "privacyPolicySectionResponsible": "Responsible Party", + "privacyPolicySectionResponsibleContent": "Your Name/Company\nYour Address\nEmail: privacy@example.com\nPhone: +49 123 456789", + + "privacyPolicySectionDataCollected": "Data We Collect", + "privacyPolicySectionDataCollectedContent": "• Email address (for account management)\n• Encrypted password\n• Training data (weights, repetitions, dates)\n• Bodyweight\n• Avatar settings\n• Device information (OS version, app version)", + + "privacyPolicySectionLegalBasis": "Legal Basis", + "privacyPolicySectionLegalBasisContent": "Art. 6 para. 1 lit. b GDPR - Contract fulfillment\nThe processing of your data is necessary to provide you with the app's functionalities and to store your training progress.", + + "privacyPolicySectionStorageDuration": "Storage Duration", + "privacyPolicySectionStorageDurationContent": "Your data will be stored until you delete your account. After account deletion, all your data will be permanently removed from our servers within 30 days.", + + "privacyPolicySectionYourRights": "Your Rights", + "privacyPolicySectionYourRightsContent": "You have the following rights under GDPR:\n\n• Right to access your stored data\n• Right to rectification of incorrect data\n• Right to erasure (\"right to be forgotten\")\n• Right to data portability (export as JSON)\n• Right to withdraw consent\n• Right to lodge a complaint with a supervisory authority", + + "privacyPolicySectionDataSharing": "Data Sharing", + "privacyPolicySectionDataSharingContent": "Your data is stored on our servers located in Germany. We do not share your personal data with third parties, except:\n\n• When required by law\n• With your explicit consent\n• For technical service providers (hosting) under strict data processing agreements", + + "privacyPolicySectionSecurity": "Data Security", + "privacyPolicySectionSecurityContent": "We implement industry-standard security measures:\n\n• End-to-end encryption for data transmission (TLS/SSL)\n• Encrypted password storage (bcrypt)\n• Regular security audits\n• Secure server infrastructure\n• Access controls and authentication", + + "privacyPolicySectionDeletion": "Account Deletion", + "privacyPolicySectionDeletionContent": "You can delete your account at any time in the app settings. All your personal data will be permanently deleted within 30 days. Training data will be anonymized for statistical purposes only if you explicitly agree.", + + "privacyPolicySectionContact": "Contact & Questions", + "privacyPolicySectionContactContent": "If you have questions about data protection or want to exercise your rights, please contact us at:\n\nprivacy@example.com", + + "privacyPolicySectionUpdates": "Policy Updates", + "privacyPolicySectionUpdatesContent": "We may update this privacy policy from time to time. You will be notified of significant changes within the app. Last updated: January 2026", + + "exportMyData": "Export My Data", + "deleteMyAccount": "Delete My Account", + "privacyPolicyButton": "Privacy Policy", + + "exportDataSuccess": "Data exported successfully", + "exportDataError": "Error exporting data: {error}", + "@exportDataError": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "deleteAccountConfirmTitle": "Delete Account?", + "deleteAccountConfirmBody": "This action cannot be undone. All your data will be permanently deleted within 30 days.\n\nAre you sure you want to continue?", + "deleteAccountFinalWarning": "Type \"DELETE\" to confirm:", + "deleteAccountSuccess": "Account deleted successfully", + "deleteAccountError": "Error deleting account: {error}", + "@deleteAccountError": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "deleteAccountConfirmationMismatch": "Confirmation text does not match", + + "consentAcceptPrefix": "I accept the ", + "consentPrivacyPolicy": "Privacy Policy", + "consentAnd": " and ", + "consentTermsOfService": "Terms of Service", + "consentRequired": "You must accept the privacy policy and terms to continue", + "termsOfServiceTitle": "Terms of Service" } diff --git a/lib/main.dart b/lib/main.dart index df186ee..f4e3f04 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -10,11 +12,11 @@ void main() async { try { await dotenv.load(fileName: '.env'); - debugPrint('Environment loaded: ${dotenv.env['ENVIRONMENT']}'); - debugPrint('API URL: ${dotenv.env['API_BASE_URL']}'); + log('Environment loaded: ${dotenv.env['ENVIRONMENT']}'); + log('API URL: ${dotenv.env['API_BASE_URL']}'); } catch (e) { - debugPrint('Could not load .env file: $e'); - debugPrint('Using default production values'); + log('Could not load .env file: $e'); + log('Using default production values'); } await SystemChrome.setPreferredOrientations([ diff --git a/lib/src/app.dart b/lib/src/app.dart index f40f8b8..e6e49f4 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:slrpg_app/l10n/app_localizations.dart'; diff --git a/lib/src/core/constants/app_constants.dart b/lib/src/core/constants/app_constants.dart index 977d621..bf7f81e 100644 --- a/lib/src/core/constants/app_constants.dart +++ b/lib/src/core/constants/app_constants.dart @@ -1,8 +1,9 @@ -import 'package:flutter/material.dart'; +import 'dart:developer'; + import 'package:flutter_dotenv/flutter_dotenv.dart'; class AppConstants { - // ✅ API Configuration aus Environment + // API Configuration aus Environment static String get apiBaseUrl => dotenv.env['API_BASE_URL'] ?? 'https://slift.patanix.de'; @@ -13,19 +14,19 @@ class AppConstants { static bool get isDebugMode => dotenv.env['DEBUG_MODE']?.toLowerCase() == 'true'; - // ✅ Helper Getter + // Helper Getter static bool get isDevelopment => environment == 'development'; static bool get isProduction => environment == 'production'; // Debug Info static void printConfig() { - debugPrint('═══════════════════════════════════'); - debugPrint('🔧 APP CONFIGURATION'); - debugPrint('Environment: $environment'); - debugPrint('API Base URL: $apiBaseUrl'); - debugPrint('API Version: $apiVersion'); - debugPrint('Debug Mode: $isDebugMode'); - debugPrint('═══════════════════════════════════'); + log('═══════════════════════════════════'); + log('🔧 APP CONFIGURATION'); + log('Environment: $environment'); + log('API Base URL: $apiBaseUrl'); + log('API Version: $apiVersion'); + log('Debug Mode: $isDebugMode'); + log('═══════════════════════════════════'); } // API Configuration // static const String apiBaseUrl = 'http://10.0.2.2:8090'; // Android emulator diff --git a/lib/src/core/routing/app_router.dart b/lib/src/core/routing/app_router.dart index 2dd6c42..b415ddf 100644 --- a/lib/src/core/routing/app_router.dart +++ b/lib/src/core/routing/app_router.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:slrpg_app/src/features/multiplayer/presentation/screens/leaderboard_screen.dart'; import 'package:slrpg_app/src/features/multiplayer/presentation/screens/lobby_screen.dart'; +import 'package:slrpg_app/src/features/settings/presentation/screens/privacy_policy_screen.dart'; import '../../features/authentication/presentation/screens/login_screen.dart'; import '../../features/authentication/presentation/screens/profile_screen.dart'; @@ -33,6 +34,7 @@ final routerProvider = Provider((ref) { final isOnAuthPage = state.matchedLocation == '/login' || state.matchedLocation == '/register' || + state.matchedLocation == '/privacy-policy' || state.matchedLocation.startsWith('/onboarding'); if (!isAuthenticated && @@ -161,6 +163,11 @@ final routerProvider = Provider((ref) { partyId: partyId); }, ), + GoRoute( + path: '/privacy-policy', + name: 'privacy-policy', + builder: (context, state) => const PrivacyPolicyScreen(), + ), ], ); }); diff --git a/lib/src/features/authentication/presentation/screens/profile_screen.dart b/lib/src/features/authentication/presentation/screens/profile_screen.dart index a4ca385..21df2d8 100644 --- a/lib/src/features/authentication/presentation/screens/profile_screen.dart +++ b/lib/src/features/authentication/presentation/screens/profile_screen.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:slrpg_app/l10n/app_localizations.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/data/repositories/user_repository.dart'; @@ -356,7 +357,7 @@ class _ProfileScreenState extends ConsumerState { final key = settings['accessory_template'] as String?; if (key == 'hypertrophy') return AccessoryTemplate.hypertrophy; if (key == 'conditioning') return AccessoryTemplate.conditioning; - if (key == 'journey_pullup') return AccessoryTemplate.journey_pullup; + if (key == 'journey_pullup') return AccessoryTemplate.journeyPullup; return AccessoryTemplate.none; } @@ -370,7 +371,7 @@ class _ProfileScreenState extends ConsumerState { if (newTemplate == AccessoryTemplate.conditioning) { templateKey = 'conditioning'; } - if (newTemplate == AccessoryTemplate.journey_pullup) { + if (newTemplate == AccessoryTemplate.journeyPullup) { templateKey = 'journey_pullup'; } @@ -400,6 +401,7 @@ class _ProfileScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final userRepo = ref.watch(userRepositoryProvider); final avatarConfig = _user?.avatarConfig != null ? AvatarConfig.fromJson(_user!.avatarConfig!) @@ -539,6 +541,12 @@ class _ProfileScreenState extends ConsumerState { trailing: const Icon(Icons.chevron_right), onTap: _showChangePasswordDialog, ), + ListTile( + leading: const Icon(Icons.privacy_tip_outlined), + title: Text(l10n.privacyPolicyButton), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: () => context.push('/privacy-policy'), + ), const Divider(), const SizedBox(height: 24), Text('Danger Zone', @@ -668,7 +676,7 @@ class _ProfileScreenState extends ConsumerState { ), ), _RadioTile( - value: AccessoryTemplate.journey_pullup, + value: AccessoryTemplate.journeyPullup, groupValue: current, title: 'Quest: The First Pull-Up', subtitle: 'Specific progression to master your bodyweight.', diff --git a/lib/src/features/authentication/presentation/screens/register_screen.dart b/lib/src/features/authentication/presentation/screens/register_screen.dart index 637d64d..373e114 100644 --- a/lib/src/features/authentication/presentation/screens/register_screen.dart +++ b/lib/src/features/authentication/presentation/screens/register_screen.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:slrpg_app/l10n/app_localizations.dart'; +import 'package:slrpg_app/src/shared/presentation/widgets/consent_checkbox.dart'; -import '../../../../core/theme/app_theme.dart'; import '../../../onboarding/presentation/screens/bodyweight_input_screen.dart'; class RegisterScreen extends ConsumerStatefulWidget { @@ -19,6 +19,10 @@ class _RegisterScreenState extends ConsumerState { final _emailController = TextEditingController(); final _emailFocusNode = FocusNode(); + bool _consentAccepted = false; + String? _consentError; + String? _errorMessage; + @override void dispose() { _usernameController.dispose(); @@ -29,6 +33,19 @@ class _RegisterScreenState extends ConsumerState { void _handleRegister() { FocusScope.of(context).unfocus(); + setState(() { + _consentError = null; + _errorMessage = null; + }); + if (!_consentAccepted) { + setState(() { + _consentError = AppLocalizations.of(context)!.consentRequired; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(_consentError!), backgroundColor: Colors.red), + ); + return; + } if (!_formKey.currentState!.validate()) { return; @@ -123,7 +140,29 @@ class _RegisterScreenState extends ConsumerState { return null; }, ), - const SizedBox(height: 32), + const SizedBox(height: 24), + ConsentCheckbox( + value: _consentAccepted, + onChanged: (val) { + setState(() { + _consentAccepted = val; + if (val) _consentError = null; + }); + }, + ), + if (_consentError != null) + Padding( + padding: + const EdgeInsets.only(left: 12, top: 4), + child: Text( + _consentError!, + style: TextStyle( + color: + Theme.of(context).colorScheme.error, + fontSize: 12), + ), + ), + const SizedBox(height: 24), ElevatedButton( onPressed: _handleRegister, style: ElevatedButton.styleFrom( diff --git a/lib/src/features/backup/domain/backup_service.dart b/lib/src/features/backup/domain/backup_service.dart new file mode 100644 index 0000000..4851bdc --- /dev/null +++ b/lib/src/features/backup/domain/backup_service.dart @@ -0,0 +1,44 @@ +// lib/src/features/backup/domain/backup_service.dart +import 'dart:convert'; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; +import 'package:slrpg_app/src/shared/data/repositories/cycle_repository.dart'; +import 'package:slrpg_app/src/shared/data/repositories/user_repository.dart'; +import 'package:slrpg_app/src/shared/data/repositories/workout_repository.dart'; +import '../../../shared/data/local/app_database.dart'; + +class BackupService { + final AppDatabase _db; + final UserRepository _userRepo; + final CycleRepository _cycleRepository; + final WorkoutRepository _workoutRepository; + + BackupService( + this._db, this._userRepo, this._cycleRepository, this._workoutRepository); + + Future createBackup() async { + final userData = await _userRepo.getLocalUser(); + final workouts = await _workoutRepository.getAllWorkouts(); + final cycles = await _cycleRepository.getAllCycles(); + final inventory = await _userRepo.getInventorySettingsAsync(); + + final backupData = { + 'version': '1.0.0', + 'exportDate': DateTime.now().toIso8601String(), + 'user': userData?.toJson(), + 'workouts': workouts.map((w) => w.toJson()).toList(), + 'cycles': cycles.map((c) => c.toJson()).toList(), + 'inventory': inventory, + }; + + final jsonString = const JsonEncoder.withIndent(' ').convert(backupData); + + final directory = await getTemporaryDirectory(); + final timestamp = DateTime.now().millisecondsSinceEpoch; + final file = File('${directory.path}/slrpg_export_$timestamp.json'); + + await file.writeAsString(jsonString); + + return file; + } +} diff --git a/lib/src/features/backup/domain/backup_service_provider.dart b/lib/src/features/backup/domain/backup_service_provider.dart new file mode 100644 index 0000000..7cfc4d2 --- /dev/null +++ b/lib/src/features/backup/domain/backup_service_provider.dart @@ -0,0 +1,15 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:slrpg_app/main.dart'; +import 'package:slrpg_app/src/shared/data/repositories/cycle_repository.dart'; +import 'package:slrpg_app/src/shared/data/repositories/user_repository.dart'; +import 'package:slrpg_app/src/shared/data/repositories/workout_repository.dart'; +import 'backup_service.dart'; + +final backupServiceProvider = Provider((ref) { + return BackupService( + ref.watch(appDatabaseProvider), + ref.watch(userRepositoryProvider), + ref.watch(cycleRepositoryProvider), + ref.watch(workoutRepositoryProvider), + ); +}); diff --git a/lib/src/features/dashboard/presentation/screens/hub_screen.dart b/lib/src/features/dashboard/presentation/screens/hub_screen.dart index 60fb332..15a53aa 100644 --- a/lib/src/features/dashboard/presentation/screens/hub_screen.dart +++ b/lib/src/features/dashboard/presentation/screens/hub_screen.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -14,8 +16,6 @@ import '../../../../shared/data/repositories/workout_repository.dart'; import '../../../../shared/data/remote/sync_service.dart'; import '../../../../shared/domain/logic/xp_calculator.dart'; import '../../../../shared/domain/logic/wendler_calculator.dart'; -import '../../../../shared/domain/entities/exercise.dart'; -import '../../../../shared/domain/entities/workout_set.dart'; import '../../../gamification/domain/entities/avatar_config.dart'; import '../../../gamification/domain/entities/item_catalog.dart'; import '../../../gamification/presentation/widgets/avatar_renderer.dart'; @@ -136,7 +136,7 @@ class _HubScreenState extends ConsumerState { }); } } catch (e) { - debugPrint('Failed to start workout: $e'); + log('Failed to start workout: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $e')), @@ -253,9 +253,10 @@ class _HubScreenState extends ConsumerState { await ref.read(partyRepositoryProvider).createParty(); if (mounted) context.go('/lobby/${party.id}'); } catch (e) { - if (mounted) + if (mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Error: $e'))); + } } }, child: const Text('CREATE PARTY', @@ -296,9 +297,10 @@ class _HubScreenState extends ConsumerState { await ref.read(partyRepositoryProvider).joinParty(code); if (mounted) context.go('/lobby/${party.id}'); } catch (e) { - if (mounted) + if (mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Error: $e'))); + } } } }, diff --git a/lib/src/features/gamification/application/quest_service.dart b/lib/src/features/gamification/application/quest_service.dart index 670d51b..83ac919 100644 --- a/lib/src/features/gamification/application/quest_service.dart +++ b/lib/src/features/gamification/application/quest_service.dart @@ -1,13 +1,10 @@ import 'dart:math'; -import 'package:flutter/foundation.dart'; +import 'dart:developer' as dev; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:drift/drift.dart'; import '../../../shared/data/local/app_database.dart'; import '../../../shared/data/repositories/user_repository.dart'; -import '../../../shared/data/repositories/workout_repository.dart'; import '../data/repositories/quest_repository.dart'; -import '../../../core/constants/app_constants.dart'; enum QuestTrigger { workoutComplete, @@ -48,7 +45,7 @@ class QuestService { final hasDailies = activeDailies.any((q) => q.type == 'daily'); if (!hasDailies) { - debugPrint('🎲 Generating new Daily Quests...'); + dev.log('🎲 Generating new Daily Quests...'); final random = Random(); final newQuests = []; @@ -153,8 +150,6 @@ class QuestService { } } -// --- QUEST DEFINITIONS --- - class _QuestTemplate { final String title; final String description; @@ -172,8 +167,7 @@ const List<_QuestTemplate> _dailyQuestPool = [ _QuestTemplate('Workout Warrior', 'Complete 1 Workout today.', 1, 50), _QuestTemplate('Rep Collector', 'Perform 50 total repetitions across all exercises.', 50, 75), - _QuestTemplate('Early Bird', 'Start a workout before noon.', 1, - 50), // Logik müsste Zeit prüfen + _QuestTemplate('Early Bird', 'Start a workout before noon.', 1, 50), _QuestTemplate( 'Iron Discipline', 'Log your bodyweight in the profile.', 1, 25), ]; diff --git a/lib/src/features/gamification/presentation/widgets/quest_item.dart b/lib/src/features/gamification/presentation/widgets/quest_item.dart index 6704ac5..c749f56 100644 --- a/lib/src/features/gamification/presentation/widgets/quest_item.dart +++ b/lib/src/features/gamification/presentation/widgets/quest_item.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/data/local/app_database.dart'; import '../../data/repositories/quest_repository.dart'; -import '../../domain/entities/item_catalog.dart'; class QuestItem extends ConsumerStatefulWidget { final QuestCollection quest; diff --git a/lib/src/features/history/presentation/screens/history_screen.dart b/lib/src/features/history/presentation/screens/history_screen.dart index 85c8917..63f4df7 100644 --- a/lib/src/features/history/presentation/screens/history_screen.dart +++ b/lib/src/features/history/presentation/screens/history_screen.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -101,7 +103,7 @@ class _WorkoutHistoryCard extends StatelessWidget { .map((json) => Exercise.fromJson(json as Map)) .toList(); } catch (e) { - debugPrint('Error parsing workout history: $e'); + log('Error parsing workout history: $e'); return []; } } diff --git a/lib/src/features/multiplayer/data/repositories/leaderboard_repository.dart b/lib/src/features/multiplayer/data/repositories/leaderboard_repository.dart index 227b048..f77a954 100644 --- a/lib/src/features/multiplayer/data/repositories/leaderboard_repository.dart +++ b/lib/src/features/multiplayer/data/repositories/leaderboard_repository.dart @@ -1,4 +1,3 @@ -import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:slrpg_app/src/features/multiplayer/domain/entities/leaderboard_entry.dart'; import 'package:slrpg_app/src/shared/data/repositories/user_repository.dart'; diff --git a/lib/src/features/multiplayer/data/repositories/party_repository.dart b/lib/src/features/multiplayer/data/repositories/party_repository.dart index 846949b..fb99bf1 100644 --- a/lib/src/features/multiplayer/data/repositories/party_repository.dart +++ b/lib/src/features/multiplayer/data/repositories/party_repository.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; +import 'dart:developer'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pocketbase/pocketbase.dart'; import 'package:slrpg_app/src/features/multiplayer/domain/entities/party.dart'; @@ -133,7 +133,7 @@ class PartyRepository { await _pb.collection('party_members').delete(memberId); } } catch (e) { - debugPrint('Error leaving party: $e'); + log('Error leaving party: $e'); } } } diff --git a/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart b/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart index 34ae10b..95acdaf 100644 --- a/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart +++ b/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart @@ -1,8 +1,6 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:slrpg_app/src/core/constants/app_constants.dart'; import 'package:slrpg_app/src/features/multiplayer/domain/entities/leaderboard_entry.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/data/repositories/user_repository.dart'; diff --git a/lib/src/features/multiplayer/presentation/screens/lobby_screen.dart b/lib/src/features/multiplayer/presentation/screens/lobby_screen.dart index 747bb3b..6b90acd 100644 --- a/lib/src/features/multiplayer/presentation/screens/lobby_screen.dart +++ b/lib/src/features/multiplayer/presentation/screens/lobby_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter/services.dart'; -import 'package:slrpg_app/src/features/workout_runner/application/workout_generator_service.dart'; import 'package:slrpg_app/src/shared/data/repositories/workout_repository.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/data/repositories/user_repository.dart'; diff --git a/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart b/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart index 451fcdb..52946b2 100644 --- a/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart @@ -9,7 +9,6 @@ import 'package:slrpg_app/l10n/app_localizations.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/data/repositories/user_repository.dart'; import '../../../../shared/data/repositories/cycle_repository.dart'; -import '../../../../shared/data/local/app_database.dart'; import '../../../gamification/domain/entities/avatar_config.dart'; import '../../../gamification/presentation/widgets/avatar_editor.dart'; import 'bodyweight_input_screen.dart'; diff --git a/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart b/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart index 1ff6e42..249110f 100644 --- a/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/inventory_setup_screen.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -7,7 +9,6 @@ import '../../../../core/theme/app_theme.dart'; import '../../../../core/constants/app_constants.dart'; import '../../../../shared/data/repositories/user_repository.dart'; import '../../../../shared/data/repositories/cycle_repository.dart'; -import '../../../../core/constants/asset_paths.dart'; import '../../../inventory/presentation/widgets/plate_counter.dart'; import 'bodyweight_input_screen.dart'; @@ -151,7 +152,7 @@ class _InventorySetupScreenState extends ConsumerState { inventorySettings: inventorySettings, ); - debugPrint('✅ User registered: ${user.serverId}'); + log('✅ User registered: ${user.serverId}'); final trainingMaxes = onboardingData['training_maxes'] as Map?; @@ -164,9 +165,9 @@ class _InventorySetupScreenState extends ConsumerState { 'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0, }; - debugPrint('📊 Creating cycle with TMs: $tmMap'); + log('📊 Creating cycle with TMs: $tmMap'); await cycleRepo.createCycle(tmMap); - debugPrint('✅ Cycle created successfully'); + log('✅ Cycle created successfully'); } if (mounted) { @@ -174,8 +175,8 @@ class _InventorySetupScreenState extends ConsumerState { context.go('/hub'); } } catch (e, stackTrace) { - debugPrint('❌ Setup failed: $e'); - debugPrint('Stack trace: $stackTrace'); + log('❌ Setup failed: $e'); + log('Stack trace: $stackTrace'); if (mounted) { String message = 'Setup failed: ${e.toString()}'; diff --git a/lib/src/features/settings/presentation/screens/privacy_policy_screen.dart b/lib/src/features/settings/presentation/screens/privacy_policy_screen.dart new file mode 100644 index 0000000..6c10af7 --- /dev/null +++ b/lib/src/features/settings/presentation/screens/privacy_policy_screen.dart @@ -0,0 +1,312 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:go_router/go_router.dart'; +import 'package:slrpg_app/l10n/app_localizations.dart'; +import 'package:slrpg_app/src/shared/data/local/app_database.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/data/repositories/user_repository.dart'; +import '../../../backup/domain/backup_service_provider.dart'; + +class PrivacyPolicyScreen extends ConsumerStatefulWidget { + const PrivacyPolicyScreen({super.key}); + + @override + ConsumerState createState() => + _PrivacyPolicyScreenState(); +} + +class _PrivacyPolicyScreenState extends ConsumerState { + bool _isExporting = false; + bool _isDeleting = false; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final userRepo = ref.watch(userRepositoryProvider); + + return Scaffold( + appBar: AppBar( + title: Text(l10n.privacyPolicyTitle), + ), + body: FutureBuilder( + future: userRepo.getLocalUser(), + builder: (context, snapshot) { + final isLoggedIn = snapshot.data != null; + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSection( + l10n.privacyPolicySectionResponsible, + l10n.privacyPolicySectionResponsibleContent, + ), + _buildSection( + l10n.privacyPolicySectionDataCollected, + l10n.privacyPolicySectionDataCollectedContent, + ), + _buildSection( + l10n.privacyPolicySectionLegalBasis, + l10n.privacyPolicySectionLegalBasisContent, + ), + _buildSection( + l10n.privacyPolicySectionStorageDuration, + l10n.privacyPolicySectionStorageDurationContent, + ), + _buildSection( + l10n.privacyPolicySectionYourRights, + l10n.privacyPolicySectionYourRightsContent, + ), + _buildSection( + l10n.privacyPolicySectionDataSharing, + l10n.privacyPolicySectionDataSharingContent, + ), + _buildSection( + l10n.privacyPolicySectionSecurity, + l10n.privacyPolicySectionSecurityContent, + ), + _buildSection( + l10n.privacyPolicySectionDeletion, + l10n.privacyPolicySectionDeletionContent, + ), + _buildSection( + l10n.privacyPolicySectionContact, + l10n.privacyPolicySectionContactContent, + ), + _buildSection( + l10n.privacyPolicySectionUpdates, + l10n.privacyPolicySectionUpdatesContent, + ), + if (isLoggedIn) ...[ + const SizedBox(height: 32), + const Divider(), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _isExporting ? null : _exportUserData, + icon: _isExporting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.black, + ), + ) + : const Icon(Icons.download), + label: Text(l10n.exportMyData), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _isDeleting ? null : _showDeleteAccountDialog, + icon: _isDeleting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.red, + ), + ) + : const Icon(Icons.delete_forever, color: Colors.red), + label: Text( + l10n.deleteMyAccount, + style: const TextStyle(color: Colors.red), + ), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + side: const BorderSide(color: Colors.red), + ), + ), + ), + const SizedBox(height: 24), + ], + ], + ), + ); + }, + ), + ); + } + + Widget _buildSection(String title, String content) { + return Padding( + padding: const EdgeInsets.only(bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(height: 8), + Text( + content, + style: const TextStyle( + height: 1.6, + fontSize: 14, + color: AppTheme.textSecondary, + ), + ), + ], + ), + ); + } + + Future _exportUserData() async { + final l10n = AppLocalizations.of(context)!; + + setState(() => _isExporting = true); + + try { + final backupService = ref.read(backupServiceProvider); + final file = await backupService.createBackup(); + + await Share.shareXFiles( + [XFile(file.path)], + subject: 'SLRPG Data Export', + text: 'Your SLRPG training data (GDPR export)', + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.exportDataSuccess), + backgroundColor: AppTheme.successColor, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.exportDataError(e.toString())), + backgroundColor: AppTheme.errorColor, + ), + ); + } + } finally { + if (mounted) { + setState(() => _isExporting = false); + } + } + } + + Future _showDeleteAccountDialog() async { + final l10n = AppLocalizations.of(context)!; + final confirmationController = TextEditingController(); + final expectedText = l10n.localeName == 'de' ? 'LÖSCHEN' : 'DELETE'; + + final confirmed = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: Text(l10n.deleteAccountConfirmTitle), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.deleteAccountConfirmBody), + const SizedBox(height: 24), + Text( + l10n.deleteAccountFinalWarning, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + const SizedBox(height: 8), + TextField( + controller: confirmationController, + decoration: InputDecoration( + hintText: expectedText, + border: const OutlineInputBorder(), + ), + textCapitalization: TextCapitalization.characters, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.cancelButton), + ), + TextButton( + onPressed: () { + if (confirmationController.text.trim().toUpperCase() == + expectedText) { + Navigator.of(context).pop(true); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.deleteAccountConfirmationMismatch), + backgroundColor: AppTheme.errorColor, + ), + ); + } + }, + child: Text( + l10n.deleteMyAccount, + style: const TextStyle(color: Colors.red), + ), + ), + ], + ), + ); + + confirmationController.dispose(); + + if (confirmed == true && mounted) { + await _deleteAccount(); + } + } + + Future _deleteAccount() async { + final l10n = AppLocalizations.of(context)!; + + setState(() => _isDeleting = true); + + try { + await ref.read(userRepositoryProvider).deleteAccount(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.deleteAccountSuccess), + backgroundColor: AppTheme.successColor, + ), + ); + + await Future.delayed(const Duration(seconds: 2)); + if (mounted) { + context.go('/login'); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.deleteAccountError(e.toString())), + backgroundColor: AppTheme.errorColor, + ), + ); + } + } finally { + if (mounted) { + setState(() => _isDeleting = false); + } + } + } +} diff --git a/lib/src/features/stats/presentation/screens/stats_screen.dart b/lib/src/features/stats/presentation/screens/stats_screen.dart index 3e85acc..de9d183 100644 --- a/lib/src/features/stats/presentation/screens/stats_screen.dart +++ b/lib/src/features/stats/presentation/screens/stats_screen.dart @@ -1,8 +1,9 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:slrpg_app/l10n/app_localizations.dart'; -import 'package:slrpg_app/src/shared/data/local/tables.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/data/local/app_database.dart'; @@ -120,7 +121,7 @@ class _StatsScreenState extends ConsumerState { }); } } catch (e) { - debugPrint('Failed to calculate local stats: $e'); + log('Failed to calculate local stats: $e'); if (mounted) { setState(() { _chartData = []; diff --git a/lib/src/features/workout_runner/application/battle_state.dart b/lib/src/features/workout_runner/application/battle_state.dart new file mode 100644 index 0000000..8582415 --- /dev/null +++ b/lib/src/features/workout_runner/application/battle_state.dart @@ -0,0 +1,118 @@ +import 'package:equatable/equatable.dart'; +import 'package:slrpg_app/src/shared/domain/entities/exercise.dart'; +import 'package:slrpg_app/src/shared/domain/entities/workout_set.dart'; + +class BattleState extends Equatable { + final List exercises; + final int currentExerciseIndex; + final int currentSetIndex; + final int repsCompleted; + final bool isLoading; + final bool isResting; + final int restSeconds; + final BattleStatus status; + final String? errorMessage; + + const BattleState({ + this.exercises = const [], + this.currentExerciseIndex = 0, + this.currentSetIndex = 0, + this.repsCompleted = 0, + this.isLoading = true, + this.isResting = false, + this.restSeconds = 0, + this.status = BattleStatus.initial, + this.errorMessage, + }); + + // Computed properties + Exercise? get currentExercise { + if (exercises.isEmpty || currentExerciseIndex >= exercises.length) { + return null; + } + return exercises[currentExerciseIndex]; + } + + WorkoutSet? get currentSet { + final exercise = currentExercise; + if (exercise == null || currentSetIndex >= exercise.sets.length) { + return null; + } + return exercise.sets[currentSetIndex]; + } + + bool get hasNextSet { + final exercise = currentExercise; + if (exercise == null) return false; + return currentSetIndex < exercise.sets.length - 1; + } + + bool get hasNextExercise { + return currentExerciseIndex < exercises.length - 1; + } + + int get totalHP { + return exercises.fold( + 0, + (sum, ex) => sum + ex.sets.fold(0, (s, set) => s + set.repsTarget), + ); + } + + int get completedHP { + return exercises.take(currentExerciseIndex).fold( + 0, + (sum, ex) => + sum + ex.sets.fold(0, (s, set) => s + set.repsActual), + ) + + (currentExercise?.sets + .take(currentSetIndex) + .fold(0, (sum, set) => sum + set.repsActual) ?? + 0); + } + + BattleState copyWith({ + List? exercises, + int? currentExerciseIndex, + int? currentSetIndex, + int? repsCompleted, + bool? isLoading, + bool? isResting, + int? restSeconds, + BattleStatus? status, + String? errorMessage, + }) { + return BattleState( + exercises: exercises ?? this.exercises, + currentExerciseIndex: currentExerciseIndex ?? this.currentExerciseIndex, + currentSetIndex: currentSetIndex ?? this.currentSetIndex, + repsCompleted: repsCompleted ?? this.repsCompleted, + isLoading: isLoading ?? this.isLoading, + isResting: isResting ?? this.isResting, + restSeconds: restSeconds ?? this.restSeconds, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [ + exercises, + currentExerciseIndex, + currentSetIndex, + repsCompleted, + isLoading, + isResting, + restSeconds, + status, + errorMessage, + ]; +} + +enum BattleStatus { + initial, + loading, + ready, + resting, + completed, + error, +} diff --git a/lib/src/features/workout_runner/application/workout_generator_service.dart b/lib/src/features/workout_runner/application/workout_generator_service.dart index c2ae329..1d976ed 100644 --- a/lib/src/features/workout_runner/application/workout_generator_service.dart +++ b/lib/src/features/workout_runner/application/workout_generator_service.dart @@ -30,7 +30,7 @@ class WorkoutGeneratorService { ? conditioningSets : 15; exercises.addAll(_generateConditioning(day, sets)); - } else if (template == AccessoryTemplate.journey_pullup) { + } else if (template == AccessoryTemplate.journeyPullup) { exercises.addAll(_generatePullUpJourney(day, trainingMaxes)); } @@ -152,7 +152,7 @@ class WorkoutGeneratorService { name: 'KB Snatch', sets: 10, intervalSeconds: 60, - repsPerSet: 10)); + repsPerSet: 5)); break; case 2: // Dip Tag (Push) @@ -169,13 +169,14 @@ class WorkoutGeneratorService { accessories.add(createSimple('curl', 'Barbell Curl', 3, 10, weight: calculateWeight(pullupTm, 0.2))); + accessories.add(createSimple('plank', 'Plank (30s)', 3, 1)); + accessories.add(_createIntervalExercise( id: 'kb_swing', name: '2H KB Swing', sets: 10, intervalSeconds: 60, - repsPerSet: 5)); - accessories.add(createSimple('plank', 'Plank (30s)', 3, 1)); + repsPerSet: 10)); break; } return accessories; @@ -213,7 +214,7 @@ class WorkoutGeneratorService { switch (day) { case 1: exercises.add(createAccessory('scap_pull', 'Scapular Pull-Ups', - ExerciseType.scapular_pull, 3, 10)); + ExerciseType.scapularPull, 3, 10)); exercises.add(createAccessory( 'plank', 'Core Plank (45s)', ExerciseType.plank, 3, 1)); @@ -221,21 +222,21 @@ class WorkoutGeneratorService { case 2: exercises.add(createAccessory( - 'inv_row', 'Australian Pull-Ups', ExerciseType.inverted_row, 4, 8)); + 'inv_row', 'Australian Pull-Ups', ExerciseType.invertedRow, 4, 8)); exercises.add(createAccessory( - 'face_pull', 'Band Face Pull', ExerciseType.face_pull, 3, 15)); + 'face_pull', 'Band Face Pull', ExerciseType.facePull, 3, 15)); break; case 3: exercises.add(createAccessory('neg_pull', 'Negative Pull-Ups (5s slow)', - ExerciseType.negative_pullup, 3, 4)); + ExerciseType.negativePullup, 3, 4)); final rowTm = trainingMaxes['row'] ?? 0.0; final curlWeight = rowTm > 0 ? calculateWeight(rowTm, 0.3) : 0.0; exercises.add(createAccessory( - 'curl', 'Barbell Curl', ExerciseType.curl_barbell, 3, 10, + 'curl', 'Barbell Curl', ExerciseType.curlBarbell, 3, 10, weight: curlWeight)); break; } 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 1b8d68b..5973233 100644 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -42,9 +44,9 @@ class _BattleScreenState extends ConsumerState { void initState() { super.initState(); if (widget.partyId != null) { - debugPrint("⚔️ MULTIPLAYER BATTLE STARTED! Party ID: ${widget.partyId}"); + log("⚔️ MULTIPLAYER BATTLE STARTED! Party ID: ${widget.partyId}"); } else { - debugPrint("👤 SINGLEPLAYER BATTLE"); + log("👤 SINGLEPLAYER BATTLE"); } _loadWorkout(); } diff --git a/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart b/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart index e81a43e..2189521 100644 --- a/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart +++ b/lib/src/features/workout_runner/presentation/widgets/emom_timer_widget.dart @@ -1,8 +1,8 @@ import 'dart:async'; +import 'dart:developer'; import 'package:flutter/material.dart'; import '../../../../core/theme/app_theme.dart'; import 'package:audioplayers/audioplayers.dart'; -import '../../../../core/constants/asset_paths.dart'; class EmomTimerWidget extends StatefulWidget { final int intervalSeconds; @@ -81,7 +81,7 @@ class _EmomTimerWidgetState extends State } await _audioPlayer.play(AssetSource(path)); } catch (e) { - debugPrint('Audio error: $e'); + log('Audio error: $e'); } } diff --git a/lib/src/shared/data/remote/sync_service.dart b/lib/src/shared/data/remote/sync_service.dart index 2dcd380..5f125db 100644 --- a/lib/src/shared/data/remote/sync_service.dart +++ b/lib/src/shared/data/remote/sync_service.dart @@ -1,5 +1,4 @@ -import 'dart:convert'; -import 'package:flutter/foundation.dart'; +import 'dart:developer'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:drift/drift.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -29,7 +28,7 @@ class SyncService { _isSyncing = true; try { - debugPrint('🔄 Starting Sync...'); + log('🔄 Starting Sync...'); final dirtyCycles = await (db.select(db.cycles) ..where((c) => c.isDirty.equals(true))) @@ -38,7 +37,7 @@ class SyncService { for (var cycle in dirtyCycles) { try { if (cycle.serverId == null) { - debugPrint('📤 Pushing new cycle ${cycle.cycleNumber}...'); + log('📤 Pushing new cycle ${cycle.cycleNumber}...'); final tmsMap = cycle.trainingMaxes .map((k, v) => MapEntry(k, (v as num).toDouble())); @@ -69,7 +68,7 @@ class SyncService { .write(const CyclesCompanion(isDirty: Value(false))); } } catch (e) { - debugPrint('❌ Failed to sync cycle ${cycle.id}: $e'); + log('❌ Failed to sync cycle ${cycle.id}: $e'); } } @@ -111,7 +110,7 @@ class SyncService { if ((pushData['workouts'] as List).isNotEmpty || pushData['user_stats'] != null) { - debugPrint('📤 Pushing data...'); + log('📤 Pushing data...'); final response = await apiClient.sync( lastSyncTimestamp: lastSync ?? '', pushData: pushData, @@ -164,7 +163,7 @@ class SyncService { if (response['pull_data']['workouts'] != null) { final pulledWorkouts = response['pull_data']['workouts'] as List; - debugPrint('📥 Pulled ${pulledWorkouts.length} workouts.'); + log('📥 Pulled ${pulledWorkouts.length} workouts.'); for (var wJson in pulledWorkouts) { final serverId = wJson['id'] as String; @@ -185,8 +184,7 @@ class SyncService { .get(); if (candidates.isNotEmpty) { existing = candidates.first; - debugPrint( - '🔄 Merging local workout ${existing.id} with server ID $serverId'); + log('🔄 Merging local workout ${existing.id} with server ID $serverId'); } } @@ -226,10 +224,10 @@ class SyncService { value: response['server_timestamp']); } } - debugPrint('✅ Sync completed successfully'); + log('✅ Sync completed successfully'); } catch (e, stack) { - debugPrint('❌ Sync failed: $e'); - debugPrint(stack.toString()); + log('❌ Sync failed: $e'); + log(stack.toString()); } finally { _isSyncing = false; } diff --git a/lib/src/shared/data/repositories/cycle_repository.dart b/lib/src/shared/data/repositories/cycle_repository.dart index 6108771..9c99fd8 100644 --- a/lib/src/shared/data/repositories/cycle_repository.dart +++ b/lib/src/shared/data/repositories/cycle_repository.dart @@ -1,7 +1,7 @@ -import 'package:flutter/foundation.dart'; +import 'dart:developer'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:drift/drift.dart'; -import 'dart:convert'; import '../local/app_database.dart'; import '../remote/api_client.dart'; @@ -24,6 +24,10 @@ class CycleRepository { Future getCurrentCycle() async { return await (db.select(db.cycles) ..where((c) => c.isActive.equals(true)) + ..orderBy([ + (t) => + OrderingTerm(expression: t.cycleNumber, mode: OrderingMode.desc) + ]) ..limit(1)) .getSingleOrNull(); } @@ -159,7 +163,7 @@ class CycleRepository { } } } catch (e) { - debugPrint('⚠️ Error checking lift success for $exerciseId: $e'); + log('⚠️ Error checking lift success for $exerciseId: $e'); } } return false; @@ -167,23 +171,23 @@ class CycleRepository { if (checkSuccess('squat')) { newTMs['squat'] = newTMs['squat']! + AppConstants.lowerBodyIncrement; - debugPrint('✅ Squat Progress: TM increased'); + log('✅ Squat Progress: TM increased'); } else { - debugPrint('⚠️ Squat Stall: TM kept same'); + log('⚠️ Squat Stall: TM kept same'); } if (checkSuccess('pullup')) { newTMs['pullup'] = newTMs['pullup']! + AppConstants.upperBodyIncrement; - debugPrint('✅ Pullup Progress: TM increased'); + log('✅ Pullup Progress: TM increased'); } else { - debugPrint('⚠️ Pullup Stall: TM kept same'); + log('⚠️ Pullup Stall: TM kept same'); } if (checkSuccess('dip')) { newTMs['dip'] = newTMs['dip']! + AppConstants.upperBodyIncrement; - debugPrint('✅ Dip Progress: TM increased'); + log('✅ Dip Progress: TM increased'); } else { - debugPrint('⚠️ Dip Stall: TM kept same'); + log('⚠️ Dip Stall: TM kept same'); } if (currentCycle.serverId != null) { diff --git a/lib/src/shared/data/repositories/user_repository.dart b/lib/src/shared/data/repositories/user_repository.dart index 6318f49..7bf78d8 100644 --- a/lib/src/shared/data/repositories/user_repository.dart +++ b/lib/src/shared/data/repositories/user_repository.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:drift/drift.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; diff --git a/lib/src/shared/domain/logic/plate_calculator.dart b/lib/src/shared/domain/logic/plate_calculator.dart index 8a665e5..b9a5d5a 100644 --- a/lib/src/shared/domain/logic/plate_calculator.dart +++ b/lib/src/shared/domain/logic/plate_calculator.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - class PlateLoadResult { final bool success; final List plateConfiguration; diff --git a/lib/src/shared/domain/logic/wendler_calculator.dart b/lib/src/shared/domain/logic/wendler_calculator.dart index b1b7059..b146865 100644 --- a/lib/src/shared/domain/logic/wendler_calculator.dart +++ b/lib/src/shared/domain/logic/wendler_calculator.dart @@ -11,26 +11,26 @@ enum ExerciseType { bench, // Hypertrophy Accessories - deadlift_romanian, - curl_barbell, - press_overhead, - face_pull, - ab_wheel, + deadliftRomanian, + curlBarbell, + pressOverhead, + facePull, + abWheel, plank, // Conditioning (Kettlebell) - kb_swing, - kb_snatch, - kb_thruster, - kb_clean_press, + kbSwing, + kbSnatch, + kbThruster, + kbCleanPress, // pullup journey - scapular_pull, - inverted_row, - negative_pullup, + scapularPull, + invertedRow, + negativePullup, } -enum AccessoryTemplate { none, hypertrophy, conditioning, journey_pullup } +enum AccessoryTemplate { none, hypertrophy, conditioning, journeyPullup } class WendlerCalculator { static const Map> weekPercentages = { diff --git a/lib/src/shared/presentation/widgets/consent_checkbox.dart b/lib/src/shared/presentation/widgets/consent_checkbox.dart new file mode 100644 index 0000000..a38bbed --- /dev/null +++ b/lib/src/shared/presentation/widgets/consent_checkbox.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:slrpg_app/src/core/theme/app_theme.dart'; +import '../../../../l10n/app_localizations.dart'; + +class ConsentCheckbox extends StatelessWidget { + final bool value; + final ValueChanged onChanged; + + const ConsentCheckbox({ + super.key, + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Checkbox( + value: value, + onChanged: (v) => onChanged(v ?? false), + activeColor: AppTheme.primaryColor, + ), + Expanded( + child: GestureDetector( + onTap: () { + onChanged(!value); + }, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text("${l10n.consentAcceptPrefix} "), + GestureDetector( + onTap: () { + context.push('/privacy-policy'); + }, + child: Text( + l10n.consentTermsOfService, + style: TextStyle( + color: AppTheme.secondaryColor, + decoration: TextDecoration.underline, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ), + ], + ); + } +}