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",
|
"usernameLabel": "Heldenname",
|
||||||
"usernameEmptyError": "Bitte wähle einen Heldennamen",
|
"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",
|
"usernameLabel": "Hero Name",
|
||||||
"usernameEmptyError": "Please choose a 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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
@ -10,11 +12,11 @@ void main() async {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dotenv.load(fileName: '.env');
|
await dotenv.load(fileName: '.env');
|
||||||
debugPrint('Environment loaded: ${dotenv.env['ENVIRONMENT']}');
|
log('Environment loaded: ${dotenv.env['ENVIRONMENT']}');
|
||||||
debugPrint('API URL: ${dotenv.env['API_BASE_URL']}');
|
log('API URL: ${dotenv.env['API_BASE_URL']}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Could not load .env file: $e');
|
log('Could not load .env file: $e');
|
||||||
debugPrint('Using default production values');
|
log('Using default production values');
|
||||||
}
|
}
|
||||||
|
|
||||||
await SystemChrome.setPreferredOrientations([
|
await SystemChrome.setPreferredOrientations([
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:slrpg_app/l10n/app_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';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
|
||||||
class AppConstants {
|
class AppConstants {
|
||||||
// ✅ API Configuration aus Environment
|
// API Configuration aus Environment
|
||||||
static String get apiBaseUrl =>
|
static String get apiBaseUrl =>
|
||||||
dotenv.env['API_BASE_URL'] ?? 'https://slift.patanix.de';
|
dotenv.env['API_BASE_URL'] ?? 'https://slift.patanix.de';
|
||||||
|
|
||||||
|
|
@ -13,19 +14,19 @@ class AppConstants {
|
||||||
static bool get isDebugMode =>
|
static bool get isDebugMode =>
|
||||||
dotenv.env['DEBUG_MODE']?.toLowerCase() == 'true';
|
dotenv.env['DEBUG_MODE']?.toLowerCase() == 'true';
|
||||||
|
|
||||||
// ✅ Helper Getter
|
// Helper Getter
|
||||||
static bool get isDevelopment => environment == 'development';
|
static bool get isDevelopment => environment == 'development';
|
||||||
static bool get isProduction => environment == 'production';
|
static bool get isProduction => environment == 'production';
|
||||||
|
|
||||||
// Debug Info
|
// Debug Info
|
||||||
static void printConfig() {
|
static void printConfig() {
|
||||||
debugPrint('═══════════════════════════════════');
|
log('═══════════════════════════════════');
|
||||||
debugPrint('🔧 APP CONFIGURATION');
|
log('🔧 APP CONFIGURATION');
|
||||||
debugPrint('Environment: $environment');
|
log('Environment: $environment');
|
||||||
debugPrint('API Base URL: $apiBaseUrl');
|
log('API Base URL: $apiBaseUrl');
|
||||||
debugPrint('API Version: $apiVersion');
|
log('API Version: $apiVersion');
|
||||||
debugPrint('Debug Mode: $isDebugMode');
|
log('Debug Mode: $isDebugMode');
|
||||||
debugPrint('═══════════════════════════════════');
|
log('═══════════════════════════════════');
|
||||||
}
|
}
|
||||||
// API Configuration
|
// API Configuration
|
||||||
// static const String apiBaseUrl = 'http://10.0.2.2:8090'; // Android emulator
|
// 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: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/leaderboard_screen.dart';
|
||||||
import 'package:slrpg_app/src/features/multiplayer/presentation/screens/lobby_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/login_screen.dart';
|
||||||
import '../../features/authentication/presentation/screens/profile_screen.dart';
|
import '../../features/authentication/presentation/screens/profile_screen.dart';
|
||||||
|
|
@ -33,6 +34,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||||
|
|
||||||
final isOnAuthPage = state.matchedLocation == '/login' ||
|
final isOnAuthPage = state.matchedLocation == '/login' ||
|
||||||
state.matchedLocation == '/register' ||
|
state.matchedLocation == '/register' ||
|
||||||
|
state.matchedLocation == '/privacy-policy' ||
|
||||||
state.matchedLocation.startsWith('/onboarding');
|
state.matchedLocation.startsWith('/onboarding');
|
||||||
|
|
||||||
if (!isAuthenticated &&
|
if (!isAuthenticated &&
|
||||||
|
|
@ -161,6 +163,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||||
partyId: partyId);
|
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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:slrpg_app/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../shared/data/repositories/user_repository.dart';
|
import '../../../../shared/data/repositories/user_repository.dart';
|
||||||
|
|
@ -356,7 +357,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
final key = settings['accessory_template'] as String?;
|
final key = settings['accessory_template'] as String?;
|
||||||
if (key == 'hypertrophy') return AccessoryTemplate.hypertrophy;
|
if (key == 'hypertrophy') return AccessoryTemplate.hypertrophy;
|
||||||
if (key == 'conditioning') return AccessoryTemplate.conditioning;
|
if (key == 'conditioning') return AccessoryTemplate.conditioning;
|
||||||
if (key == 'journey_pullup') return AccessoryTemplate.journey_pullup;
|
if (key == 'journey_pullup') return AccessoryTemplate.journeyPullup;
|
||||||
return AccessoryTemplate.none;
|
return AccessoryTemplate.none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -370,7 +371,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
if (newTemplate == AccessoryTemplate.conditioning) {
|
if (newTemplate == AccessoryTemplate.conditioning) {
|
||||||
templateKey = 'conditioning';
|
templateKey = 'conditioning';
|
||||||
}
|
}
|
||||||
if (newTemplate == AccessoryTemplate.journey_pullup) {
|
if (newTemplate == AccessoryTemplate.journeyPullup) {
|
||||||
templateKey = 'journey_pullup';
|
templateKey = 'journey_pullup';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -400,6 +401,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final userRepo = ref.watch(userRepositoryProvider);
|
final userRepo = ref.watch(userRepositoryProvider);
|
||||||
final avatarConfig = _user?.avatarConfig != null
|
final avatarConfig = _user?.avatarConfig != null
|
||||||
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
||||||
|
|
@ -539,6 +541,12 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: _showChangePasswordDialog,
|
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 Divider(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text('Danger Zone',
|
Text('Danger Zone',
|
||||||
|
|
@ -668,7 +676,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_RadioTile<AccessoryTemplate>(
|
_RadioTile<AccessoryTemplate>(
|
||||||
value: AccessoryTemplate.journey_pullup,
|
value: AccessoryTemplate.journeyPullup,
|
||||||
groupValue: current,
|
groupValue: current,
|
||||||
title: 'Quest: The First Pull-Up',
|
title: 'Quest: The First Pull-Up',
|
||||||
subtitle: 'Specific progression to master your bodyweight.',
|
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:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:slrpg_app/l10n/app_localizations.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';
|
import '../../../onboarding/presentation/screens/bodyweight_input_screen.dart';
|
||||||
|
|
||||||
class RegisterScreen extends ConsumerStatefulWidget {
|
class RegisterScreen extends ConsumerStatefulWidget {
|
||||||
|
|
@ -19,6 +19,10 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
final _emailFocusNode = FocusNode();
|
final _emailFocusNode = FocusNode();
|
||||||
|
|
||||||
|
bool _consentAccepted = false;
|
||||||
|
String? _consentError;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_usernameController.dispose();
|
_usernameController.dispose();
|
||||||
|
|
@ -29,6 +33,19 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||||
|
|
||||||
void _handleRegister() {
|
void _handleRegister() {
|
||||||
FocusScope.of(context).unfocus();
|
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()) {
|
if (!_formKey.currentState!.validate()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -123,7 +140,29 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||||
return null;
|
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(
|
ElevatedButton(
|
||||||
onPressed: _handleRegister,
|
onPressed: _handleRegister,
|
||||||
style: ElevatedButton.styleFrom(
|
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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.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/data/remote/sync_service.dart';
|
||||||
import '../../../../shared/domain/logic/xp_calculator.dart';
|
import '../../../../shared/domain/logic/xp_calculator.dart';
|
||||||
import '../../../../shared/domain/logic/wendler_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/avatar_config.dart';
|
||||||
import '../../../gamification/domain/entities/item_catalog.dart';
|
import '../../../gamification/domain/entities/item_catalog.dart';
|
||||||
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
||||||
|
|
@ -136,7 +136,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to start workout: $e');
|
log('Failed to start workout: $e');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Error: $e')),
|
SnackBar(content: Text('Error: $e')),
|
||||||
|
|
@ -253,10 +253,11 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
await ref.read(partyRepositoryProvider).createParty();
|
await ref.read(partyRepositoryProvider).createParty();
|
||||||
if (mounted) context.go('/lobby/${party.id}');
|
if (mounted) context.go('/lobby/${party.id}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted)
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
.showSnackBar(SnackBar(content: Text('Error: $e')));
|
.showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: const Text('CREATE PARTY',
|
child: const Text('CREATE PARTY',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
@ -296,11 +297,12 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
await ref.read(partyRepositoryProvider).joinParty(code);
|
await ref.read(partyRepositoryProvider).joinParty(code);
|
||||||
if (mounted) context.go('/lobby/${party.id}');
|
if (mounted) context.go('/lobby/${party.id}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted)
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
.showSnackBar(SnackBar(content: Text('Error: $e')));
|
.showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: const Text('JOIN'),
|
child: const Text('JOIN'),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'dart:developer' as dev;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
|
|
||||||
import '../../../shared/data/local/app_database.dart';
|
import '../../../shared/data/local/app_database.dart';
|
||||||
import '../../../shared/data/repositories/user_repository.dart';
|
import '../../../shared/data/repositories/user_repository.dart';
|
||||||
import '../../../shared/data/repositories/workout_repository.dart';
|
|
||||||
import '../data/repositories/quest_repository.dart';
|
import '../data/repositories/quest_repository.dart';
|
||||||
import '../../../core/constants/app_constants.dart';
|
|
||||||
|
|
||||||
enum QuestTrigger {
|
enum QuestTrigger {
|
||||||
workoutComplete,
|
workoutComplete,
|
||||||
|
|
@ -48,7 +45,7 @@ class QuestService {
|
||||||
final hasDailies = activeDailies.any((q) => q.type == 'daily');
|
final hasDailies = activeDailies.any((q) => q.type == 'daily');
|
||||||
|
|
||||||
if (!hasDailies) {
|
if (!hasDailies) {
|
||||||
debugPrint('🎲 Generating new Daily Quests...');
|
dev.log('🎲 Generating new Daily Quests...');
|
||||||
final random = Random();
|
final random = Random();
|
||||||
final newQuests = <QuestCollection>[];
|
final newQuests = <QuestCollection>[];
|
||||||
|
|
||||||
|
|
@ -153,8 +150,6 @@ class QuestService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- QUEST DEFINITIONS ---
|
|
||||||
|
|
||||||
class _QuestTemplate {
|
class _QuestTemplate {
|
||||||
final String title;
|
final String title;
|
||||||
final String description;
|
final String description;
|
||||||
|
|
@ -172,8 +167,7 @@ const List<_QuestTemplate> _dailyQuestPool = [
|
||||||
_QuestTemplate('Workout Warrior', 'Complete 1 Workout today.', 1, 50),
|
_QuestTemplate('Workout Warrior', 'Complete 1 Workout today.', 1, 50),
|
||||||
_QuestTemplate('Rep Collector',
|
_QuestTemplate('Rep Collector',
|
||||||
'Perform 50 total repetitions across all exercises.', 50, 75),
|
'Perform 50 total repetitions across all exercises.', 50, 75),
|
||||||
_QuestTemplate('Early Bird', 'Start a workout before noon.', 1,
|
_QuestTemplate('Early Bird', 'Start a workout before noon.', 1, 50),
|
||||||
50), // Logik müsste Zeit prüfen
|
|
||||||
_QuestTemplate(
|
_QuestTemplate(
|
||||||
'Iron Discipline', 'Log your bodyweight in the profile.', 1, 25),
|
'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 '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../shared/data/local/app_database.dart';
|
import '../../../../shared/data/local/app_database.dart';
|
||||||
import '../../data/repositories/quest_repository.dart';
|
import '../../data/repositories/quest_repository.dart';
|
||||||
import '../../domain/entities/item_catalog.dart';
|
|
||||||
|
|
||||||
class QuestItem extends ConsumerStatefulWidget {
|
class QuestItem extends ConsumerStatefulWidget {
|
||||||
final QuestCollection quest;
|
final QuestCollection quest;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.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>))
|
.map((json) => Exercise.fromJson(json as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error parsing workout history: $e');
|
log('Error parsing workout history: $e');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/features/multiplayer/domain/entities/leaderboard_entry.dart';
|
||||||
import 'package:slrpg_app/src/shared/data/repositories/user_repository.dart';
|
import 'package:slrpg_app/src/shared/data/repositories/user_repository.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'dart:developer';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pocketbase/pocketbase.dart';
|
import 'package:pocketbase/pocketbase.dart';
|
||||||
import 'package:slrpg_app/src/features/multiplayer/domain/entities/party.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);
|
await _pb.collection('party_members').delete(memberId);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.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 'package:slrpg_app/src/features/multiplayer/domain/entities/leaderboard_entry.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../shared/data/repositories/user_repository.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:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:flutter/services.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 'package:slrpg_app/src/shared/data/repositories/workout_repository.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../shared/data/repositories/user_repository.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 '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../shared/data/repositories/user_repository.dart';
|
import '../../../../shared/data/repositories/user_repository.dart';
|
||||||
import '../../../../shared/data/repositories/cycle_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/domain/entities/avatar_config.dart';
|
||||||
import '../../../gamification/presentation/widgets/avatar_editor.dart';
|
import '../../../gamification/presentation/widgets/avatar_editor.dart';
|
||||||
import 'bodyweight_input_screen.dart';
|
import 'bodyweight_input_screen.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.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 '../../../../core/constants/app_constants.dart';
|
||||||
import '../../../../shared/data/repositories/user_repository.dart';
|
import '../../../../shared/data/repositories/user_repository.dart';
|
||||||
import '../../../../shared/data/repositories/cycle_repository.dart';
|
import '../../../../shared/data/repositories/cycle_repository.dart';
|
||||||
import '../../../../core/constants/asset_paths.dart';
|
|
||||||
import '../../../inventory/presentation/widgets/plate_counter.dart';
|
import '../../../inventory/presentation/widgets/plate_counter.dart';
|
||||||
import 'bodyweight_input_screen.dart';
|
import 'bodyweight_input_screen.dart';
|
||||||
|
|
||||||
|
|
@ -151,7 +152,7 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
|
||||||
inventorySettings: inventorySettings,
|
inventorySettings: inventorySettings,
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint('✅ User registered: ${user.serverId}');
|
log('✅ User registered: ${user.serverId}');
|
||||||
|
|
||||||
final trainingMaxes =
|
final trainingMaxes =
|
||||||
onboardingData['training_maxes'] as Map<String, dynamic>?;
|
onboardingData['training_maxes'] as Map<String, dynamic>?;
|
||||||
|
|
@ -164,9 +165,9 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
|
||||||
'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0,
|
'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
debugPrint('📊 Creating cycle with TMs: $tmMap');
|
log('📊 Creating cycle with TMs: $tmMap');
|
||||||
await cycleRepo.createCycle(tmMap);
|
await cycleRepo.createCycle(tmMap);
|
||||||
debugPrint('✅ Cycle created successfully');
|
log('✅ Cycle created successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -174,8 +175,8 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
|
||||||
context.go('/hub');
|
context.go('/hub');
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
debugPrint('❌ Setup failed: $e');
|
log('❌ Setup failed: $e');
|
||||||
debugPrint('Stack trace: $stackTrace');
|
log('Stack trace: $stackTrace');
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
String message = 'Setup failed: ${e.toString()}';
|
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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:slrpg_app/l10n/app_localizations.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 '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../shared/data/local/app_database.dart';
|
import '../../../../shared/data/local/app_database.dart';
|
||||||
|
|
@ -120,7 +121,7 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to calculate local stats: $e');
|
log('Failed to calculate local stats: $e');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_chartData = [];
|
_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
|
? conditioningSets
|
||||||
: 15;
|
: 15;
|
||||||
exercises.addAll(_generateConditioning(day, sets));
|
exercises.addAll(_generateConditioning(day, sets));
|
||||||
} else if (template == AccessoryTemplate.journey_pullup) {
|
} else if (template == AccessoryTemplate.journeyPullup) {
|
||||||
exercises.addAll(_generatePullUpJourney(day, trainingMaxes));
|
exercises.addAll(_generatePullUpJourney(day, trainingMaxes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ class WorkoutGeneratorService {
|
||||||
name: 'KB Snatch',
|
name: 'KB Snatch',
|
||||||
sets: 10,
|
sets: 10,
|
||||||
intervalSeconds: 60,
|
intervalSeconds: 60,
|
||||||
repsPerSet: 10));
|
repsPerSet: 5));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2: // Dip Tag (Push)
|
case 2: // Dip Tag (Push)
|
||||||
|
|
@ -169,13 +169,14 @@ class WorkoutGeneratorService {
|
||||||
accessories.add(createSimple('curl', 'Barbell Curl', 3, 10,
|
accessories.add(createSimple('curl', 'Barbell Curl', 3, 10,
|
||||||
weight: calculateWeight(pullupTm, 0.2)));
|
weight: calculateWeight(pullupTm, 0.2)));
|
||||||
|
|
||||||
|
accessories.add(createSimple('plank', 'Plank (30s)', 3, 1));
|
||||||
|
|
||||||
accessories.add(_createIntervalExercise(
|
accessories.add(_createIntervalExercise(
|
||||||
id: 'kb_swing',
|
id: 'kb_swing',
|
||||||
name: '2H KB Swing',
|
name: '2H KB Swing',
|
||||||
sets: 10,
|
sets: 10,
|
||||||
intervalSeconds: 60,
|
intervalSeconds: 60,
|
||||||
repsPerSet: 5));
|
repsPerSet: 10));
|
||||||
accessories.add(createSimple('plank', 'Plank (30s)', 3, 1));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return accessories;
|
return accessories;
|
||||||
|
|
@ -213,7 +214,7 @@ class WorkoutGeneratorService {
|
||||||
switch (day) {
|
switch (day) {
|
||||||
case 1:
|
case 1:
|
||||||
exercises.add(createAccessory('scap_pull', 'Scapular Pull-Ups',
|
exercises.add(createAccessory('scap_pull', 'Scapular Pull-Ups',
|
||||||
ExerciseType.scapular_pull, 3, 10));
|
ExerciseType.scapularPull, 3, 10));
|
||||||
|
|
||||||
exercises.add(createAccessory(
|
exercises.add(createAccessory(
|
||||||
'plank', 'Core Plank (45s)', ExerciseType.plank, 3, 1));
|
'plank', 'Core Plank (45s)', ExerciseType.plank, 3, 1));
|
||||||
|
|
@ -221,21 +222,21 @@ class WorkoutGeneratorService {
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
exercises.add(createAccessory(
|
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(
|
exercises.add(createAccessory(
|
||||||
'face_pull', 'Band Face Pull', ExerciseType.face_pull, 3, 15));
|
'face_pull', 'Band Face Pull', ExerciseType.facePull, 3, 15));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
exercises.add(createAccessory('neg_pull', 'Negative Pull-Ups (5s slow)',
|
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 rowTm = trainingMaxes['row'] ?? 0.0;
|
||||||
final curlWeight = rowTm > 0 ? calculateWeight(rowTm, 0.3) : 0.0;
|
final curlWeight = rowTm > 0 ? calculateWeight(rowTm, 0.3) : 0.0;
|
||||||
|
|
||||||
exercises.add(createAccessory(
|
exercises.add(createAccessory(
|
||||||
'curl', 'Barbell Curl', ExerciseType.curl_barbell, 3, 10,
|
'curl', 'Barbell Curl', ExerciseType.curlBarbell, 3, 10,
|
||||||
weight: curlWeight));
|
weight: curlWeight));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
@ -42,9 +44,9 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.partyId != null) {
|
if (widget.partyId != null) {
|
||||||
debugPrint("⚔️ MULTIPLAYER BATTLE STARTED! Party ID: ${widget.partyId}");
|
log("⚔️ MULTIPLAYER BATTLE STARTED! Party ID: ${widget.partyId}");
|
||||||
} else {
|
} else {
|
||||||
debugPrint("👤 SINGLEPLAYER BATTLE");
|
log("👤 SINGLEPLAYER BATTLE");
|
||||||
}
|
}
|
||||||
_loadWorkout();
|
_loadWorkout();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
import '../../../../core/constants/asset_paths.dart';
|
|
||||||
|
|
||||||
class EmomTimerWidget extends StatefulWidget {
|
class EmomTimerWidget extends StatefulWidget {
|
||||||
final int intervalSeconds;
|
final int intervalSeconds;
|
||||||
|
|
@ -81,7 +81,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
|
||||||
}
|
}
|
||||||
await _audioPlayer.play(AssetSource(path));
|
await _audioPlayer.play(AssetSource(path));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Audio error: $e');
|
log('Audio error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:convert';
|
import 'dart:developer';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
@ -29,7 +28,7 @@ class SyncService {
|
||||||
_isSyncing = true;
|
_isSyncing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
debugPrint('🔄 Starting Sync...');
|
log('🔄 Starting Sync...');
|
||||||
|
|
||||||
final dirtyCycles = await (db.select(db.cycles)
|
final dirtyCycles = await (db.select(db.cycles)
|
||||||
..where((c) => c.isDirty.equals(true)))
|
..where((c) => c.isDirty.equals(true)))
|
||||||
|
|
@ -38,7 +37,7 @@ class SyncService {
|
||||||
for (var cycle in dirtyCycles) {
|
for (var cycle in dirtyCycles) {
|
||||||
try {
|
try {
|
||||||
if (cycle.serverId == null) {
|
if (cycle.serverId == null) {
|
||||||
debugPrint('📤 Pushing new cycle ${cycle.cycleNumber}...');
|
log('📤 Pushing new cycle ${cycle.cycleNumber}...');
|
||||||
final tmsMap = cycle.trainingMaxes
|
final tmsMap = cycle.trainingMaxes
|
||||||
.map((k, v) => MapEntry(k, (v as num).toDouble()));
|
.map((k, v) => MapEntry(k, (v as num).toDouble()));
|
||||||
|
|
||||||
|
|
@ -69,7 +68,7 @@ class SyncService {
|
||||||
.write(const CyclesCompanion(isDirty: Value(false)));
|
.write(const CyclesCompanion(isDirty: Value(false)));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 ||
|
if ((pushData['workouts'] as List).isNotEmpty ||
|
||||||
pushData['user_stats'] != null) {
|
pushData['user_stats'] != null) {
|
||||||
debugPrint('📤 Pushing data...');
|
log('📤 Pushing data...');
|
||||||
final response = await apiClient.sync(
|
final response = await apiClient.sync(
|
||||||
lastSyncTimestamp: lastSync ?? '',
|
lastSyncTimestamp: lastSync ?? '',
|
||||||
pushData: pushData,
|
pushData: pushData,
|
||||||
|
|
@ -164,7 +163,7 @@ class SyncService {
|
||||||
|
|
||||||
if (response['pull_data']['workouts'] != null) {
|
if (response['pull_data']['workouts'] != null) {
|
||||||
final pulledWorkouts = response['pull_data']['workouts'] as List;
|
final pulledWorkouts = response['pull_data']['workouts'] as List;
|
||||||
debugPrint('📥 Pulled ${pulledWorkouts.length} workouts.');
|
log('📥 Pulled ${pulledWorkouts.length} workouts.');
|
||||||
|
|
||||||
for (var wJson in pulledWorkouts) {
|
for (var wJson in pulledWorkouts) {
|
||||||
final serverId = wJson['id'] as String;
|
final serverId = wJson['id'] as String;
|
||||||
|
|
@ -185,8 +184,7 @@ class SyncService {
|
||||||
.get();
|
.get();
|
||||||
if (candidates.isNotEmpty) {
|
if (candidates.isNotEmpty) {
|
||||||
existing = candidates.first;
|
existing = candidates.first;
|
||||||
debugPrint(
|
log('🔄 Merging local workout ${existing.id} with server ID $serverId');
|
||||||
'🔄 Merging local workout ${existing.id} with server ID $serverId');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,10 +224,10 @@ class SyncService {
|
||||||
value: response['server_timestamp']);
|
value: response['server_timestamp']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debugPrint('✅ Sync completed successfully');
|
log('✅ Sync completed successfully');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
debugPrint('❌ Sync failed: $e');
|
log('❌ Sync failed: $e');
|
||||||
debugPrint(stack.toString());
|
log(stack.toString());
|
||||||
} finally {
|
} finally {
|
||||||
_isSyncing = false;
|
_isSyncing = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import '../local/app_database.dart';
|
import '../local/app_database.dart';
|
||||||
import '../remote/api_client.dart';
|
import '../remote/api_client.dart';
|
||||||
|
|
@ -24,6 +24,10 @@ class CycleRepository {
|
||||||
Future<CycleCollection?> getCurrentCycle() async {
|
Future<CycleCollection?> getCurrentCycle() async {
|
||||||
return await (db.select(db.cycles)
|
return await (db.select(db.cycles)
|
||||||
..where((c) => c.isActive.equals(true))
|
..where((c) => c.isActive.equals(true))
|
||||||
|
..orderBy([
|
||||||
|
(t) =>
|
||||||
|
OrderingTerm(expression: t.cycleNumber, mode: OrderingMode.desc)
|
||||||
|
])
|
||||||
..limit(1))
|
..limit(1))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +163,7 @@ class CycleRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ Error checking lift success for $exerciseId: $e');
|
log('⚠️ Error checking lift success for $exerciseId: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -167,23 +171,23 @@ class CycleRepository {
|
||||||
|
|
||||||
if (checkSuccess('squat')) {
|
if (checkSuccess('squat')) {
|
||||||
newTMs['squat'] = newTMs['squat']! + AppConstants.lowerBodyIncrement;
|
newTMs['squat'] = newTMs['squat']! + AppConstants.lowerBodyIncrement;
|
||||||
debugPrint('✅ Squat Progress: TM increased');
|
log('✅ Squat Progress: TM increased');
|
||||||
} else {
|
} else {
|
||||||
debugPrint('⚠️ Squat Stall: TM kept same');
|
log('⚠️ Squat Stall: TM kept same');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkSuccess('pullup')) {
|
if (checkSuccess('pullup')) {
|
||||||
newTMs['pullup'] = newTMs['pullup']! + AppConstants.upperBodyIncrement;
|
newTMs['pullup'] = newTMs['pullup']! + AppConstants.upperBodyIncrement;
|
||||||
debugPrint('✅ Pullup Progress: TM increased');
|
log('✅ Pullup Progress: TM increased');
|
||||||
} else {
|
} else {
|
||||||
debugPrint('⚠️ Pullup Stall: TM kept same');
|
log('⚠️ Pullup Stall: TM kept same');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkSuccess('dip')) {
|
if (checkSuccess('dip')) {
|
||||||
newTMs['dip'] = newTMs['dip']! + AppConstants.upperBodyIncrement;
|
newTMs['dip'] = newTMs['dip']! + AppConstants.upperBodyIncrement;
|
||||||
debugPrint('✅ Dip Progress: TM increased');
|
log('✅ Dip Progress: TM increased');
|
||||||
} else {
|
} else {
|
||||||
debugPrint('⚠️ Dip Stall: TM kept same');
|
log('⚠️ Dip Stall: TM kept same');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentCycle.serverId != null) {
|
if (currentCycle.serverId != null) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
class PlateLoadResult {
|
class PlateLoadResult {
|
||||||
final bool success;
|
final bool success;
|
||||||
final List<double> plateConfiguration;
|
final List<double> plateConfiguration;
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,26 @@ enum ExerciseType {
|
||||||
bench,
|
bench,
|
||||||
|
|
||||||
// Hypertrophy Accessories
|
// Hypertrophy Accessories
|
||||||
deadlift_romanian,
|
deadliftRomanian,
|
||||||
curl_barbell,
|
curlBarbell,
|
||||||
press_overhead,
|
pressOverhead,
|
||||||
face_pull,
|
facePull,
|
||||||
ab_wheel,
|
abWheel,
|
||||||
plank,
|
plank,
|
||||||
|
|
||||||
// Conditioning (Kettlebell)
|
// Conditioning (Kettlebell)
|
||||||
kb_swing,
|
kbSwing,
|
||||||
kb_snatch,
|
kbSnatch,
|
||||||
kb_thruster,
|
kbThruster,
|
||||||
kb_clean_press,
|
kbCleanPress,
|
||||||
|
|
||||||
// pullup journey
|
// pullup journey
|
||||||
scapular_pull,
|
scapularPull,
|
||||||
inverted_row,
|
invertedRow,
|
||||||
negative_pullup,
|
negativePullup,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AccessoryTemplate { none, hypertrophy, conditioning, journey_pullup }
|
enum AccessoryTemplate { none, hypertrophy, conditioning, journeyPullup }
|
||||||
|
|
||||||
class WendlerCalculator {
|
class WendlerCalculator {
|
||||||
static const Map<int, List<double>> weekPercentages = {
|
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