slrpg-app/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart

233 lines
7.5 KiB
Dart

import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:drift/drift.dart' hide Column;
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 '../../../gamification/domain/entities/avatar_config.dart';
import '../../../gamification/presentation/widgets/avatar_editor.dart';
import 'bodyweight_input_screen.dart';
class AvatarSetupScreen extends ConsumerStatefulWidget {
const AvatarSetupScreen({super.key});
@override
ConsumerState<AvatarSetupScreen> createState() => _AvatarSetupScreenState();
}
class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
AvatarConfig _config = const AvatarConfig();
bool _isLoading = false;
Future<void> _handleFinish() async {
final password = await _showPasswordDialog();
if (password == null) return;
setState(() => _isLoading = true);
try {
final onboardingData = ref.read(onboardingDataProvider);
final userRepo = ref.read(userRepositoryProvider);
final inventorySettings =
(onboardingData['inventory_settings'] as Map<String, dynamic>?) ?? {};
final exerciseVariants =
onboardingData['exercise_variants'] as Map<String, dynamic>?;
var user = await userRepo.getLocalUser();
final avatarJson = _config.toJson();
if (user == null) {
final email = onboardingData['email'] as String? ?? '';
final String username = onboardingData['username'] as String;
final bodyweight =
(onboardingData['bodyweight'] as num?)?.toDouble() ?? 80.0;
if (email.isEmpty || password.isEmpty) {
throw Exception('Email or password is missing!');
}
user = await userRepo.register(
email: email,
username: username,
password: password,
bodyweight: bodyweight,
inventorySettings: inventorySettings,
exerciseVariants: exerciseVariants,
avatarConfig: avatarJson,
);
await Future.delayed(const Duration(milliseconds: 100));
user = await userRepo.getLocalUser();
await ref.read(apiClientProvider).requestVerification(email);
if (user == null) {
throw Exception(
'User registration succeeded but user not found in DB');
}
} else {
user = user.copyWith(
currentBodyweight:
(onboardingData['bodyweight'] as num?)?.toDouble() ??
user.currentBodyweight,
inventorySettings: Value(inventorySettings),
isDirty: true,
);
await userRepo.saveLocalUser(user);
}
user = user.copyWith(
avatarConfig: Value(avatarJson),
isDirty: true,
);
await userRepo.saveLocalUser(user);
try {
final trainingMaxes =
onboardingData['training_maxes'] as Map<String, dynamic>?;
if (trainingMaxes != null && trainingMaxes.isNotEmpty) {
final cycleRepo = ref.read(cycleRepositoryProvider);
final tmMap = <String, double>{
'squat': (trainingMaxes['squat'] as num?)?.toDouble() ?? 100.0,
'pullup': (trainingMaxes['pullup'] as num?)?.toDouble() ?? 80.0,
'dip': (trainingMaxes['dip'] as num?)?.toDouble() ?? 90.0,
};
final cycle = await cycleRepo.createCycle(tmMap);
}
} catch (e, stackTrace) {
log('❌ CYCLE ERROR (non-critical): $e');
log(' Error type: ${e.runtimeType}');
log(' Stack:\n$stackTrace');
}
if (mounted) {
ref.read(onboardingDataProvider.notifier).clear();
context.go('/hub');
}
} catch (e, stackTrace) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Setup failed: $e'),
backgroundColor: AppTheme.errorColor,
duration: const Duration(seconds: 5),
),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.setupAvatarTitle),
actions: [
TextButton(
onPressed: _isLoading ? null : _handleFinish,
child: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2))
: Text(l10n.finishButton,
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor)),
)
],
),
body: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
color: AppTheme.surfaceColor,
width: double.infinity,
child: Text(
l10n.setupAvatarSubtitle,
textAlign: TextAlign.center,
style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey),
),
),
Expanded(
child: AvatarEditor(
initialConfig: _config,
onChanged: (newConfig) => _config = newConfig,
),
),
],
),
);
}
Future<String?> _showPasswordDialog() async {
final passwordController = TextEditingController();
final confirmController = TextEditingController();
final formKey = GlobalKey<FormState>();
final l10n = AppLocalizations.of(context)!;
return showDialog<String>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text(l10n.secureAccountTitle),
content: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.secureAccountBody),
const SizedBox(height: 16),
TextFormField(
controller: passwordController,
obscureText: true,
autofocus: true,
decoration: InputDecoration(
labelText: l10n.passwordLabel,
prefixIcon: Icon(Icons.lock),
),
validator: (v) =>
(v?.length ?? 0) < 8 ? 'Min 8 characters' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: confirmController,
obscureText: true,
decoration: InputDecoration(
labelText: l10n.confirmPasswordLabel,
prefixIcon: Icon(Icons.lock_outline),
),
validator: (v) => v != passwordController.text
? l10n.passwordsDoNotMatch
: null,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.cancelButton),
),
ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
Navigator.pop(context, passwordController.text);
}
},
child: Text(l10n.confirmButton),
),
],
),
);
}
}