feat: add privacy policy to register and profile screen
This commit is contained in:
parent
ab3d0e9c15
commit
1e573904f2
32 changed files with 817 additions and 103 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<GoRouter>((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<GoRouter>((ref) {
|
|||
partyId: partyId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/privacy-policy',
|
||||
name: 'privacy-policy',
|
||||
builder: (context, state) => const PrivacyPolicyScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<ProfileScreen> {
|
|||
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<ProfileScreen> {
|
|||
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<ProfileScreen> {
|
|||
|
||||
@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<ProfileScreen> {
|
|||
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<ProfileScreen> {
|
|||
),
|
||||
),
|
||||
_RadioTile<AccessoryTemplate>(
|
||||
value: AccessoryTemplate.journey_pullup,
|
||||
value: AccessoryTemplate.journeyPullup,
|
||||
groupValue: current,
|
||||
title: 'Quest: The First Pull-Up',
|
||||
subtitle: 'Specific progression to master your bodyweight.',
|
||||
|
|
|
|||
|
|
@ -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<RegisterScreen> {
|
|||
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<RegisterScreen> {
|
|||
|
||||
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<RegisterScreen> {
|
|||
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(
|
||||
|
|
|
|||
44
lib/src/features/backup/domain/backup_service.dart
Normal file
44
lib/src/features/backup/domain/backup_service.dart
Normal file
|
|
@ -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<File> 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;
|
||||
}
|
||||
}
|
||||
15
lib/src/features/backup/domain/backup_service_provider.dart
Normal file
15
lib/src/features/backup/domain/backup_service_provider.dart
Normal file
|
|
@ -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<BackupService>((ref) {
|
||||
return BackupService(
|
||||
ref.watch(appDatabaseProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(cycleRepositoryProvider),
|
||||
ref.watch(workoutRepositoryProvider),
|
||||
);
|
||||
});
|
||||
|
|
@ -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<HubScreen> {
|
|||
});
|
||||
}
|
||||
} 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,10 +253,11 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
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',
|
||||
style: TextStyle(
|
||||
|
|
@ -296,11 +297,12 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
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')));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('JOIN'),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 = <QuestCollection>[];
|
||||
|
||||
|
|
@ -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),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing workout history: $e');
|
||||
log('Error parsing workout history: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<InventorySetupScreen> {
|
|||
inventorySettings: inventorySettings,
|
||||
);
|
||||
|
||||
debugPrint('✅ User registered: ${user.serverId}');
|
||||
log('✅ User registered: ${user.serverId}');
|
||||
|
||||
final trainingMaxes =
|
||||
onboardingData['training_maxes'] as Map<String, dynamic>?;
|
||||
|
|
@ -164,9 +165,9 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
|
|||
'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<InventorySetupScreen> {
|
|||
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()}';
|
||||
|
|
|
|||
|
|
@ -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<PrivacyPolicyScreen> createState() =>
|
||||
_PrivacyPolicyScreenState();
|
||||
}
|
||||
|
||||
class _PrivacyPolicyScreenState extends ConsumerState<PrivacyPolicyScreen> {
|
||||
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<UserCollection?>(
|
||||
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<void> _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<void> _showDeleteAccountDialog() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final confirmationController = TextEditingController();
|
||||
final expectedText = l10n.localeName == 'de' ? 'LÖSCHEN' : 'DELETE';
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
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<void> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<StatsScreen> {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to calculate local stats: $e');
|
||||
log('Failed to calculate local stats: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_chartData = [];
|
||||
|
|
|
|||
118
lib/src/features/workout_runner/application/battle_state.dart
Normal file
118
lib/src/features/workout_runner/application/battle_state.dart
Normal file
|
|
@ -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<Exercise> 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<int>(
|
||||
0,
|
||||
(sum, ex) => sum + ex.sets.fold<int>(0, (s, set) => s + set.repsTarget),
|
||||
);
|
||||
}
|
||||
|
||||
int get completedHP {
|
||||
return exercises.take(currentExerciseIndex).fold<int>(
|
||||
0,
|
||||
(sum, ex) =>
|
||||
sum + ex.sets.fold<int>(0, (s, set) => s + set.repsActual),
|
||||
) +
|
||||
(currentExercise?.sets
|
||||
.take(currentSetIndex)
|
||||
.fold<int>(0, (sum, set) => sum + set.repsActual) ??
|
||||
0);
|
||||
}
|
||||
|
||||
BattleState copyWith({
|
||||
List<Exercise>? 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<Object?> get props => [
|
||||
exercises,
|
||||
currentExerciseIndex,
|
||||
currentSetIndex,
|
||||
repsCompleted,
|
||||
isLoading,
|
||||
isResting,
|
||||
restSeconds,
|
||||
status,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
|
||||
enum BattleStatus {
|
||||
initial,
|
||||
loading,
|
||||
ready,
|
||||
resting,
|
||||
completed,
|
||||
error,
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<BattleScreen> {
|
|||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EmomTimerWidget>
|
|||
}
|
||||
await _audioPlayer.play(AssetSource(path));
|
||||
} catch (e) {
|
||||
debugPrint('Audio error: $e');
|
||||
log('Audio error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CycleCollection?> 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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:math';
|
||||
|
||||
class PlateLoadResult {
|
||||
final bool success;
|
||||
final List<double> plateConfiguration;
|
||||
|
|
|
|||
|
|
@ -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<int, List<double>> weekPercentages = {
|
||||
|
|
|
|||
57
lib/src/shared/presentation/widgets/consent_checkbox.dart
Normal file
57
lib/src/shared/presentation/widgets/consent_checkbox.dart
Normal file
|
|
@ -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<bool> 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue