small fix, don't worry about it

This commit is contained in:
Tomas Dvorak
2026-04-10 12:05:40 +02:00
parent 7b7ed0083f
commit 5ab2773f98
55 changed files with 3240 additions and 483 deletions
@@ -1,24 +1,30 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
import '../../../data/models/user_model.dart' as app;
import '../../../data/repositories/user_repository.dart';
import '../../../bootstrap/supabase_client.dart';
import '../../../core/errors/failure.dart';
import '../../../core/utils/unit_conversion_utils.dart';
final profileControllerProvider = StateNotifierProvider<ProfileController, ProfileState>((ref) {
final client = supabase.Supabase.instance.client;
final repository = UserRepository(client);
final client = supabaseClient;
final repository = client != null ? UserRepository(client) : null;
return ProfileController(repository);
});
class ProfileController extends StateNotifier<ProfileState> {
final UserRepository _repository;
final UserRepository? _repository;
ProfileController(this._repository) : super(const ProfileState.initial());
Future<void> loadProfile(String userId) async {
if (_repository == null) {
state = const ProfileState.error('Profile is unavailable while offline.');
return;
}
state = const ProfileState.loading();
try {
final user = await _repository.getProfile(userId);
final user = await _repository!.getProfile(userId);
state = ProfileState.loaded(user);
} on Failure catch (failure) {
state = ProfileState.error(failure.message);
@@ -28,15 +34,20 @@ class ProfileController extends StateNotifier<ProfileState> {
}
Future<void> updateUsername(String userId, String username) async {
if (_repository == null) {
state = const ProfileState.error('Profile is unavailable while offline.');
return;
}
state = const ProfileState.loading();
try {
final isAvailable = await _repository.isUsernameAvailable(username);
final isAvailable = await _repository!.isUsernameAvailable(username);
if (!isAvailable) {
state = const ProfileState.error('Username is already taken');
return;
}
final updatedUser = await _repository.updateProfile(
final updatedUser = await _repository!.updateProfile(
userId: userId,
username: username,
);
@@ -49,9 +60,14 @@ class ProfileController extends StateNotifier<ProfileState> {
}
Future<void> updateBio(String userId, String bio) async {
if (_repository == null) {
state = const ProfileState.error('Profile is unavailable while offline.');
return;
}
state = const ProfileState.loading();
try {
final updatedUser = await _repository.updateProfile(
final updatedUser = await _repository!.updateProfile(
userId: userId,
bio: bio,
);
@@ -64,9 +80,14 @@ class ProfileController extends StateNotifier<ProfileState> {
}
Future<void> updateAvatarUrl(String userId, String avatarUrl) async {
if (_repository == null) {
state = const ProfileState.error('Profile is unavailable while offline.');
return;
}
state = const ProfileState.loading();
try {
final updatedUser = await _repository.updateProfile(
final updatedUser = await _repository!.updateProfile(
userId: userId,
avatarUrl: avatarUrl,
);
@@ -79,12 +100,17 @@ class ProfileController extends StateNotifier<ProfileState> {
}
Future<void> toggleProfileVisibility(String userId) async {
if (_repository == null) {
state = const ProfileState.error('Profile is unavailable while offline.');
return;
}
final currentState = state;
if (currentState.user == null) return;
state = const ProfileState.loading();
try {
final updatedUser = await _repository.updateProfile(
final updatedUser = await _repository!.updateProfile(
userId: userId,
isPublicProfile: !currentState.user!.isPublicProfile,
);
@@ -105,16 +131,27 @@ class ProfileController extends StateNotifier<ProfileState> {
String? instagramHandle,
String? tiktokHandle,
String? websiteUrl,
Gender? gender,
DateTime? birthDate,
double? heightCm,
double? weightKg,
HeightUnit heightUnit = HeightUnit.metric,
WeightUnit weightUnit = WeightUnit.metric,
}) async {
if (_repository == null) {
state = const ProfileState.error('Profile is unavailable while offline.');
return;
}
state = const ProfileState.loading();
try {
final isAvailable = await _repository.isUsernameAvailable(username);
final isAvailable = await _repository!.isUsernameAvailable(username);
if (!isAvailable) {
state = const ProfileState.error('Username is already taken');
return;
}
final updatedUser = await _repository.updateProfile(
final updatedUser = await _repository!.updateProfile(
userId: userId,
username: username,
bio: bio,
@@ -123,6 +160,12 @@ class ProfileController extends StateNotifier<ProfileState> {
instagramHandle: instagramHandle,
tiktokHandle: tiktokHandle,
websiteUrl: websiteUrl,
gender: gender,
birthDate: birthDate,
heightCm: heightCm,
weightKg: weightKg,
heightUnit: heightUnit,
weightUnit: weightUnit,
);
state = ProfileState.loaded(updatedUser);
} on Failure catch (failure) {
@@ -131,6 +174,19 @@ class ProfileController extends StateNotifier<ProfileState> {
state = ProfileState.error(e.toString());
}
}
Future<bool> isProfileSetupComplete(String userId) async {
if (_repository == null) {
return false;
}
try {
final user = await _repository!.getProfile(userId);
return user.username.isNotEmpty;
} catch (e) {
return false;
}
}
}
class ProfileState {
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
import '../../../bootstrap/supabase_client.dart';
import '../../../core/widgets/app_scaffold.dart';
import '../../../core/widgets/loading_indicator.dart';
import '../../../core/widgets/empty_state.dart';
@@ -23,7 +23,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
@override
void initState() {
super.initState();
final userId = supabase.Supabase.instance.client.auth.currentUser?.id;
final userId = currentSupabaseUserId;
if (userId != null) {
ref.read(profileControllerProvider.notifier).loadProfile(userId);
}
@@ -33,10 +33,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
Widget build(BuildContext context) {
final profileState = ref.watch(profileControllerProvider);
final achievementsState = ref.watch(achievementsControllerProvider);
final userId = supabase.Supabase.instance.client.auth.currentUser?.id;
final userId = currentSupabaseUserId;
if (userId == null) {
return AppScaffold(
title: 'Profile',
body: Semantics(
label: 'Not signed in',
child: const EmptyState(
@@ -105,6 +106,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
SliverAppBar(
expandedHeight: 200,
pinned: true,
title: const Text('Profile'),
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
@@ -408,7 +410,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
);
if (confirmed == true && context.mounted) {
await supabase.Supabase.instance.client.auth.signOut();
await signOutCurrentSupabaseUser();
if (context.mounted) {
context.go('/');
}
@@ -8,6 +8,7 @@ import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
import '../../../core/widgets/app_scaffold.dart';
import '../../../core/widgets/primary_button.dart';
import '../../../core/utils/validators.dart';
import '../../../core/utils/unit_conversion_utils.dart';
import '../application/profile_controller.dart';
class ProfileSetupScreen extends ConsumerStatefulWidget {
@@ -25,6 +26,8 @@ class _ProfileSetupScreenState extends ConsumerState<ProfileSetupScreen> {
final _instagramController = TextEditingController();
final _tiktokController = TextEditingController();
final _websiteController = TextEditingController();
final _heightController = TextEditingController();
final _weightController = TextEditingController();
dynamic _avatarFile;
String? _avatarUrl;
@@ -32,6 +35,11 @@ class _ProfileSetupScreenState extends ConsumerState<ProfileSetupScreen> {
bool _isCheckingUsername = false;
bool _isUsernameAvailable = true;
String? _usernameError;
Gender? _selectedGender;
DateTime? _selectedBirthDate;
HeightUnit _selectedHeightUnit = HeightUnit.metric;
WeightUnit _selectedWeightUnit = WeightUnit.metric;
final ImagePicker _imagePicker = ImagePicker();
@@ -152,6 +160,18 @@ class _ProfileSetupScreenState extends ConsumerState<ProfileSetupScreen> {
uploadedAvatarUrl = await _uploadAvatar();
}
// Parse height and weight values
double? heightCm;
double? weightKg;
if (_heightController.text.isNotEmpty) {
heightCm = UnitConversionUtils.parseHeight(_heightController.text, _selectedHeightUnit);
}
if (_weightController.text.isNotEmpty) {
weightKg = UnitConversionUtils.parseWeight(_weightController.text, _selectedWeightUnit);
}
await ref.read(profileControllerProvider.notifier).completeProfileSetup(
userId: userId,
username: _usernameController.text.trim(),
@@ -169,6 +189,12 @@ class _ProfileSetupScreenState extends ConsumerState<ProfileSetupScreen> {
websiteUrl: _websiteController.text.trim().isEmpty
? null
: _websiteController.text.trim(),
gender: _selectedGender,
birthDate: _selectedBirthDate,
heightCm: heightCm,
weightKg: weightKg,
heightUnit: _selectedHeightUnit,
weightUnit: _selectedWeightUnit,
);
if (mounted) {
@@ -195,6 +221,8 @@ class _ProfileSetupScreenState extends ConsumerState<ProfileSetupScreen> {
_instagramController.dispose();
_tiktokController.dispose();
_websiteController.dispose();
_heightController.dispose();
_weightController.dispose();
super.dispose();
}
@@ -341,6 +369,170 @@ class _ProfileSetupScreenState extends ConsumerState<ProfileSetupScreen> {
),
enabled: !_isLoading,
),
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 16),
Text(
'Biometric Information (Optional)',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 16),
// Gender Field
DropdownButtonFormField<Gender>(
initialValue: _selectedGender,
decoration: const InputDecoration(
labelText: 'Gender',
prefixIcon: Icon(Icons.person_outline),
border: OutlineInputBorder(),
),
items: Gender.values.map((gender) {
return DropdownMenuItem(
value: gender,
child: Row(
children: [
Text(gender.emoji),
const SizedBox(width: 8),
Text(gender.displayName),
],
),
);
}).toList(),
onChanged: !_isLoading ? (Gender? value) {
setState(() {
_selectedGender = value;
});
} : null,
),
const SizedBox(height: 16),
// Birth Date Field
InkWell(
onTap: !_isLoading ? () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedBirthDate ?? DateTime.now().subtract(const Duration(days: 365 * 25)),
firstDate: DateTime.now().subtract(const Duration(days: 365 * 120)),
lastDate: DateTime.now().subtract(const Duration(days: 365 * 13)),
);
if (picked != null) {
setState(() {
_selectedBirthDate = picked;
});
}
} : null,
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Birth Date',
prefixIcon: Icon(Icons.cake_outlined),
border: OutlineInputBorder(),
),
child: Text(
_selectedBirthDate != null
? '${_selectedBirthDate!.day}/${_selectedBirthDate!.month}/${_selectedBirthDate!.year}'
: 'Select your birth date',
style: TextStyle(
color: _selectedBirthDate != null
? null
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
),
),
const SizedBox(height: 16),
// Height and Weight Row
Row(
children: [
Expanded(
child: TextFormField(
controller: _heightController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Height (${_selectedHeightUnit == HeightUnit.metric ? 'cm' : 'ft/in'})',
prefixIcon: const Icon(Icons.height_outlined),
border: const OutlineInputBorder(),
suffixIcon: PopupMenuButton<HeightUnit>(
icon: const Icon(Icons.tune),
onSelected: (HeightUnit unit) {
setState(() {
_selectedHeightUnit = unit;
// Convert existing value if needed
if (_heightController.text.isNotEmpty) {
final currentValue = double.tryParse(_heightController.text);
if (currentValue != null) {
double convertedValue;
if (unit == HeightUnit.imperial && _selectedHeightUnit == HeightUnit.metric) {
convertedValue = UnitConversionUtils.cmToInches(currentValue);
_heightController.text = convertedValue.toStringAsFixed(1);
} else if (unit == HeightUnit.metric && _selectedHeightUnit == HeightUnit.imperial) {
convertedValue = UnitConversionUtils.inchesToCm(currentValue);
_heightController.text = convertedValue.toStringAsFixed(1);
}
}
}
});
},
itemBuilder: (context) => [
const PopupMenuItem(value: HeightUnit.metric, child: Text('Metric (cm)')),
const PopupMenuItem(value: HeightUnit.imperial, child: Text('Imperial (ft/in)')),
],
),
),
enabled: !_isLoading,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _weightController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Weight (${_selectedWeightUnit == WeightUnit.metric ? 'kg' : 'lbs'})',
prefixIcon: const Icon(Icons.monitor_weight_outlined),
border: const OutlineInputBorder(),
suffixIcon: PopupMenuButton<WeightUnit>(
icon: const Icon(Icons.tune),
onSelected: (WeightUnit unit) {
setState(() {
_selectedWeightUnit = unit;
// Convert existing value if needed
if (_weightController.text.isNotEmpty) {
final currentValue = double.tryParse(_weightController.text);
if (currentValue != null) {
double convertedValue;
if (unit == WeightUnit.imperial && _selectedWeightUnit == WeightUnit.metric) {
convertedValue = UnitConversionUtils.kgToLbs(currentValue);
_weightController.text = convertedValue.toStringAsFixed(1);
} else if (unit == WeightUnit.metric && _selectedWeightUnit == WeightUnit.imperial) {
convertedValue = UnitConversionUtils.lbsToKg(currentValue);
_weightController.text = convertedValue.toStringAsFixed(1);
}
}
}
});
},
itemBuilder: (context) => [
const PopupMenuItem(value: WeightUnit.metric, child: Text('Metric (kg)')),
const PopupMenuItem(value: WeightUnit.imperial, child: Text('Imperial (lbs)')),
],
),
),
enabled: !_isLoading,
),
),
],
),
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 16),
Text(
'Social Links (Optional)',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _twitterController,