diff --git a/lib/blocs/user_profile_cubit.dart b/lib/blocs/user_profile_cubit.dart new file mode 100644 index 0000000..9bc32ad --- /dev/null +++ b/lib/blocs/user_profile_cubit.dart @@ -0,0 +1,51 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:workout_ai/models/user_profile_state.dart'; +import 'package:workout_ai/services/user_profile_service.dart'; + +class UserProfileCubit extends Cubit { + final UserProfileService _profileService; + + UserProfileCubit(this._profileService) : super(const UserProfileState()); + + Future loadUserProfile() async { + try { + emit(const UserProfileState(status: UserProfileStatus.loading)); + final profile = await _profileService.getUserProfile(); + emit(UserProfileState( + status: UserProfileStatus.success, + profile: profile, + )); + } catch (e) { + emit(UserProfileState( + status: UserProfileStatus.error, + error: e.toString(), + )); + } + } + + Future updateUserProfile({ + required String username, + required String email, + required int height, + required int weight, + }) async { + try { + emit(const UserProfileState(status: UserProfileStatus.loading)); + final updatedProfile = await _profileService.updateUserProfile( + username: username, + email: email, + height: height, + weight: weight, + ); + emit(UserProfileState( + status: UserProfileStatus.success, + profile: updatedProfile, + )); + } catch (e) { + emit(UserProfileState( + status: UserProfileStatus.error, + error: e.toString(), + )); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index d1ea9b4..ef314a6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,8 @@ -// main.dart +import 'dart:async'; +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:workout_ai/models/auth_state.dart'; @@ -13,32 +16,67 @@ import 'package:workout_ai/services/auth_service.dart'; import 'package:workout_ai/views/auth/login_screen.dart'; import 'package:workout_ai/views/splash_screen.dart'; -void main() async { +Future main() async { + // Ensure Flutter is initialized WidgetsFlutterBinding.ensureInitialized(); - + + // Configure system optimizations + await _configureSystem(); + + // Initialize auth final authService = AuthService(); final isAuthenticated = await authService.initializeAuth(); - - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: Brightness.dark, - systemNavigationBarColor: Colors.white, - systemNavigationBarIconBrightness: Brightness.dark, - ), + + // Run app with error handling + runZonedGuarded( + () => runApp(MyApp(isAuthenticated: isAuthenticated)), + (error, stack) { + debugPrint('Error caught by runZonedGuarded: $error\n$stack'); + }, ); - - runApp(MyApp(isAuthenticated: isAuthenticated)); +} + +Future _configureSystem() async { + try { + // Set preferred orientations + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + // Configure UI style + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + systemNavigationBarColor: Colors.white, + systemNavigationBarIconBrightness: Brightness.dark, + ), + ); + + // Platform specific optimizations + if (Platform.isAndroid) { + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + ), + ); + } + + // Optimize Flutter rendering + SchedulerBinding.instance.ensureFrameCallbacksRegistered(); + + // Set image cache size + PaintingBinding.instance.imageCache.maximumSize = 100; + PaintingBinding.instance.imageCache.maximumSizeBytes = 50 << 20; // 50 MB + } catch (e) { + debugPrint('Error configuring system: $e'); + } } class MyApp extends StatelessWidget { final bool isAuthenticated; - + const MyApp({ super.key, this.isAuthenticated = false, @@ -47,39 +85,43 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) { - final cubit = AuthCubit(); - if (isAuthenticated) { - cubit.initializeAuth(true); - } - return cubit; - }, - lazy: false, - ), - BlocProvider( - create: (context) => UserManager(), - ), - BlocProvider( - create: (context) => WeightManager(), - ), - BlocProvider( - create: (context) => PushUpCounter(), - ), - BlocProvider( - create: (context) => SitUpCounter(), - ), - BlocProvider( - create: (context) => ExerciseCompletion(), - ), - BlocProvider( - create: (context) => ExerciseStatsModel(), - ), - ], + providers: _createBlocProviders(), child: const AppWithAuth(), ); } + + List _createBlocProviders() { + return [ + BlocProvider( + create: (context) { + final cubit = AuthCubit(); + if (isAuthenticated) { + cubit.initializeAuth(true); + } + return cubit; + }, + lazy: false, + ), + BlocProvider( + create: (context) => UserManager(), + ), + BlocProvider( + create: (context) => WeightManager(), + ), + BlocProvider( + create: (context) => PushUpCounter(), + ), + BlocProvider( + create: (context) => SitUpCounter(), + ), + BlocProvider( + create: (context) => ExerciseCompletion(), + ), + BlocProvider( + create: (context) => ExerciseStatsModel(), + ), + ]; + } } class AppWithAuth extends StatefulWidget { @@ -89,7 +131,38 @@ class AppWithAuth extends StatefulWidget { State createState() => _AppWithAuthState(); } -class _AppWithAuthState extends State { +class _AppWithAuthState extends State with WidgetsBindingObserver { + final _navigatorKey = GlobalKey(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.paused: + // Free up resources + imageCache.clear(); + imageCache.clearLiveImages(); + break; + case AppLifecycleState.resumed: + // Reinitialize if needed + setState(() {}); + break; + default: + break; + } + } + @override Widget build(BuildContext context) { return BlocBuilder( @@ -97,35 +170,65 @@ class _AppWithAuthState extends State { return MaterialApp( title: 'Workout AI', debugShowCheckedModeBanner: false, - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - navigatorKey: GlobalKey(), - onGenerateRoute: (settings) { - switch (settings.name) { - case '/': - return MaterialPageRoute( - builder: (_) => state.status == AuthStatus.authenticated - ? const SplashScreen() - : const LoginScreen(), - ); - case '/login': - return MaterialPageRoute( - builder: (_) => const LoginScreen(), - ); - case '/home': - return MaterialPageRoute( - builder: (_) => const SplashScreen(), - ); - default: - return MaterialPageRoute( - builder: (_) => const LoginScreen(), - ); - } + navigatorKey: _navigatorKey, + theme: _buildTheme(), + onGenerateRoute: (settings) => _generateRoute(settings, state), + builder: (context, child) { + // Add error boundary + return Stack( + children: [ + child ?? const SizedBox.shrink(), + if (child == null) + const Center(child: Text('Failed to load screen')), + ], + ); }, ); }, ); } -} \ No newline at end of file + + ThemeData _buildTheme() { + return ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + platform: Theme.of(context).platform, + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + }, + ), + ); + } + + Route? _generateRoute(RouteSettings settings, AuthState state) { + try { + switch (settings.name) { + case '/': + return MaterialPageRoute( + builder: (_) => state.status == AuthStatus.authenticated + ? const SplashScreen() + : const LoginScreen(), + ); + case '/login': + return MaterialPageRoute( + builder: (_) => const LoginScreen(), + ); + case '/home': + return MaterialPageRoute( + builder: (_) => const SplashScreen(), + ); + default: + return MaterialPageRoute( + builder: (_) => const LoginScreen(), + ); + } + } catch (e, stack) { + debugPrint('Error generating route: $e\n$stack'); + return MaterialPageRoute( + builder: (_) => const LoginScreen(), + ); + } + } +} diff --git a/lib/models/progress_data.dart b/lib/models/progress_data.dart index c4fe292..2aa27d3 100644 --- a/lib/models/progress_data.dart +++ b/lib/models/progress_data.dart @@ -1,12 +1,47 @@ +class ProgressResponse { + final int statusCode; + final String averageCurrentWeight; + final double caloriesBurnIn3Month; + final List data; + + ProgressResponse({ + required this.statusCode, + required this.averageCurrentWeight, + required this.caloriesBurnIn3Month, + required this.data, + }); + + factory ProgressResponse.fromJson(Map json) { + return ProgressResponse( + statusCode: json['statusCode'] as int, + averageCurrentWeight: json['averageCurrentWeight'] ?? '0.0', + caloriesBurnIn3Month: _parseDouble(json['caloriesBurnIn3Month']), + data: (json['data'] as List) + .map((item) => ProgressData.fromJson(item as Map)) + .toList(), + ); + } + + static double _parseDouble(dynamic value) { + if (value == null) return 0.0; + if (value is double) return value; + if (value is int) return value.toDouble(); + if (value is String) return double.tryParse(value) ?? 0.0; + return 0.0; + } +} + class ProgressData { final double currentWeight; final double totalCalories; final int month; + final int year; ProgressData({ required this.currentWeight, required this.totalCalories, required this.month, + required this.year, }); factory ProgressData.fromJson(Map json) { @@ -14,6 +49,7 @@ class ProgressData { currentWeight: _parseDouble(json['currentWeight']), totalCalories: _parseDouble(json['totalCalories']), month: json['month'] as int, + year: json['year'] as int, ); } @@ -25,27 +61,3 @@ class ProgressData { return 0.0; } } - -class ProgressResponse { - final int statusCode; - final String averageCurrentWeight; - final List data; - - ProgressResponse({ - required this.statusCode, - required this.averageCurrentWeight, - required this.data, - }); - - factory ProgressResponse.fromJson(Map json) { - return ProgressResponse( - statusCode: json['statusCode'] as int, - averageCurrentWeight: json['averageCurrentWeight'] as String, - data: (json['data'] as List) - .map((item) => ProgressData.fromJson(item as Map)) - .toList(), - ); - } - - double get averageWeight => double.tryParse(averageCurrentWeight) ?? 0.0; -} diff --git a/lib/models/user_profile_model.dart b/lib/models/user_profile_model.dart new file mode 100644 index 0000000..407a968 --- /dev/null +++ b/lib/models/user_profile_model.dart @@ -0,0 +1,40 @@ +// lib/models/user_profile_model.dart +class UserProfile { + final String id; + final String username; + final String email; + final String password; + final int height; + final int weight; + + const UserProfile({ + required this.id, + required this.username, + required this.email, + required this.password, + required this.height, + required this.weight, + }); + + factory UserProfile.fromJson(Map json) { + return UserProfile( + id: json['_id'] as String? ?? '', + username: json['username'] as String? ?? '', + email: json['email'] as String? ?? '', + password: json['password'] as String? ?? '', + height: json['height'] as int? ?? 0, + weight: json['weight'] as int? ?? 0, + ); + } + + Map toJson() { + return { + '_id': id, + 'username': username, + 'email': email, + 'password': password, + 'height': height, + 'weight': weight, + }; + } +} diff --git a/lib/models/user_profile_state.dart b/lib/models/user_profile_state.dart new file mode 100644 index 0000000..965df3d --- /dev/null +++ b/lib/models/user_profile_state.dart @@ -0,0 +1,28 @@ +// lib/models/user_profile_state.dart +import 'package:workout_ai/models/user_profile_model.dart'; + +enum UserProfileStatus { loading, success, error } + +class UserProfileState { + final UserProfileStatus status; + final UserProfile? profile; + final String? error; + + const UserProfileState({ + this.status = UserProfileStatus.loading, + this.profile, + this.error, + }); + + UserProfileState copyWith({ + UserProfileStatus? status, + UserProfile? profile, + String? error, + }) { + return UserProfileState( + status: status ?? this.status, + profile: profile ?? this.profile, + error: error, + ); + } +} diff --git a/lib/services/progress_service.dart b/lib/services/progress_service.dart index c63b91d..4ca35b5 100644 --- a/lib/services/progress_service.dart +++ b/lib/services/progress_service.dart @@ -1,6 +1,7 @@ import 'dart:developer'; -import 'package:workout_ai/services/api_service.dart'; + import 'package:workout_ai/models/progress_data.dart'; +import 'package:workout_ai/services/api_service.dart'; class ProgressService { final APIService _api = APIService(); diff --git a/lib/services/user_profile_service.dart b/lib/services/user_profile_service.dart new file mode 100644 index 0000000..3455ac9 --- /dev/null +++ b/lib/services/user_profile_service.dart @@ -0,0 +1,62 @@ +// lib/services/user_profile_service.dart +import 'dart:developer'; + +import 'package:workout_ai/models/user_profile_model.dart'; +import 'package:workout_ai/services/api_service.dart'; + +class UserProfileService { + final APIService _api = APIService(); + + Future getUserProfile() async { + try { + final response = await _api.get('userProfile'); + + if (response['statusCode'] != 200) { + throw Exception(response['message'] ?? 'Failed to fetch user profile'); + } + + if (response['data'] == null) { + throw Exception('User profile data is missing'); + } + + final profileData = response['data'] as Map; + return UserProfile.fromJson(profileData); + } catch (e) { + log('Error getting user profile: $e'); + rethrow; + } + } + + Future updateUserProfile({ + required String username, + required String email, + required int height, + required int weight, + }) async { + try { + final response = await _api.post( + 'userProfile', + { + 'username': username, + 'email': email, + 'height': height, + 'weight': weight, + }, + ); + + if (response['statusCode'] != 200) { + throw Exception(response['message'] ?? 'Failed to update user profile'); + } + + if (response['data'] == null) { + throw Exception('Updated profile data is missing'); + } + + final profileData = response['data'] as Map; + return UserProfile.fromJson(profileData); + } catch (e) { + log('Error updating user profile: $e'); + rethrow; + } + } +} diff --git a/lib/views/detector_base_view.dart b/lib/views/detector_base_view.dart index 90a8b38..bd7534b 100644 --- a/lib/views/detector_base_view.dart +++ b/lib/views/detector_base_view.dart @@ -20,7 +20,7 @@ class _SitUpDetectorViewState extends State { final PoseDetector _poseDetector = PoseDetector( options: PoseDetectorOptions(), ); - + bool _canProcess = true; bool _isBusy = false; CustomPaint? _customPaint; @@ -39,6 +39,7 @@ class _SitUpDetectorViewState extends State { Widget build(BuildContext context) { return DetectorView( title: 'Sit-up Counter', + exerciseTitle: 'Sit-up Counter', customPaint: _customPaint, text: _text, onImage: _processImage, @@ -59,20 +60,19 @@ class _SitUpDetectorViewState extends State { try { final poses = await _poseDetector.processImage(inputImage); - - if (inputImage.metadata?.size != null && + + if (inputImage.metadata?.size != null && inputImage.metadata?.rotation != null) { - final painter = PosePainter( poses, inputImage.metadata!.size, inputImage.metadata!.rotation, _cameraLensDirection, ); - + _customPaint = CustomPaint(painter: painter); _posePainter = painter; - + // Process sit-up detection if (poses.isNotEmpty) { final pose = poses.first; @@ -111,4 +111,4 @@ class _SitUpDetectorViewState extends State { setState(() {}); } } -} \ No newline at end of file +} diff --git a/lib/views/splash_screen.dart b/lib/views/splash_screen.dart index 65eff7c..c89d6f1 100644 --- a/lib/views/splash_screen.dart +++ b/lib/views/splash_screen.dart @@ -1,18 +1,21 @@ -// lib/views/splash_screen.dart +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:shimmer/shimmer.dart'; import 'package:workout_ai/models/auth_state.dart'; import 'package:workout_ai/models/user_manager.dart'; +import 'package:workout_ai/models/user_profile_model.dart'; import 'package:workout_ai/services/auth_service.dart'; +import 'package:workout_ai/services/user_profile_service.dart'; import 'package:workout_ai/services/workout_info_service.dart'; import 'package:workout_ai/views/auth/login_screen.dart'; import 'package:workout_ai/views/pose_detection_view.dart'; import 'package:workout_ai/views/sit_up_detector_view.dart'; import 'package:workout_ai/widgets/progress_tracker.dart'; import 'package:workout_ai/widgets/workout_card.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'dart:developer'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -26,8 +29,11 @@ class _SplashScreenState extends State late TabController _tabController; final WorkoutInfoService _workoutInfoService = WorkoutInfoService(); final AuthService _authService = AuthService(); + final UserProfileService _userProfileService = UserProfileService(); List _workoutInfo = []; + UserProfile? _userProfile; bool _isLoading = true; + bool _isLoadingProfile = true; String? _error; DateTime? _lastLoadTime; @@ -36,10 +42,48 @@ class _SplashScreenState extends State super.initState(); _tabController = TabController(length: 2, vsync: this); _tabController.addListener(_handleTabChange); - _loadData(); + _loadInitialData(); _setPortraitOrientation(); } + void _handleTabChange() { + // Add tab change handling logic here + if (_tabController.index == 1) { + _loadWorkoutInfo(); + } + } + + Future _loadUserProfile() async { + try { + final profile = await _userProfileService.getUserProfile(); + if (!mounted) return; + setState(() { + _userProfile = profile; + _isLoadingProfile = false; + }); + } catch (e) { + log('Error loading user profile: $e'); + if (!mounted) return; + setState(() { + _isLoadingProfile = false; + }); + } + } + + Future _loadInitialData() async { + try { + await Future.wait([ + _loadUserProfile(), + _loadWorkoutInfo(), + ]); + } catch (e) { + log('Error loading initial data: $e'); + if (e.toString().contains('Authentication')) { + _navigateToLogin(); + } + } + } + @override void dispose() { _tabController.removeListener(_handleTabChange); @@ -48,26 +92,33 @@ class _SplashScreenState extends State super.dispose(); } - // Add tab change handler - void _handleTabChange() { - if (!_tabController.indexIsChanging) return; - - // If switching to progress tab - if (_tabController.index == 1) { - // Check if we need to refresh - if (_shouldRefreshData()) { - _loadWorkoutInfo(); - } - } + void _startExercise(BuildContext context, Widget view) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => view), + ); } - bool _shouldRefreshData() { - if (_lastLoadTime == null) return true; - - // Refresh if last load was more than 30 seconds ago - final now = DateTime.now(); - final difference = now.difference(_lastLoadTime!); - return difference.inSeconds > 30; + Widget _buildHomeTab() { + return RefreshIndicator( + onRefresh: _loadData, + color: const Color(0xFFE8FE54), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + _buildWelcomeSection(), + const SizedBox(height: 24), + _buildQuickStats(), + const SizedBox(height: 24), + _buildWorkoutCards(), + const SizedBox(height: 24), + ], + ), + ), + ); } Future _loadWorkoutInfo() async { @@ -131,80 +182,10 @@ class _SplashScreenState extends State } } - Future _startExercise( - BuildContext context, Widget exerciseScreen) async { - try { - // Set landscape orientation before starting exercise - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); - - if (!mounted) return; - - // Navigate to exercise screen - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => exerciseScreen, - ), - ); - - // Reset orientation and reload data if needed - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - - if (result == true && mounted) { - await _loadData(); - } - } catch (e) { - debugPrint('Exercise screen error: $e'); - } - } - - String get _userName { - final user = context.read().userInfo; - return user?.username ?? ''; - } - - Widget _buildHomeTab() { - return RefreshIndicator( - onRefresh: _loadData, - color: const Color(0xFFE8FE54), - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildWelcomeSection(), - const SizedBox(height: 24), - _buildWorkoutCards(), - const SizedBox(height: 24), - // _buildInstructions(), - // const SizedBox(height: 24), - _buildQuickStats(), - const SizedBox(height: 24), - ], - ), - ), - ); - } - Widget _buildWelcomeSection() { final now = DateTime.now(); final hour = now.hour; - - // Get appropriate greeting based on time of day - String greeting; - if (hour < 12) { - greeting = 'Good morning'; - } else if (hour < 17) { - greeting = 'Good afternoon'; - } else { - greeting = 'Good evening'; - } + final greeting = _getGreeting(hour); return Container( margin: const EdgeInsets.symmetric(horizontal: 16), @@ -231,46 +212,56 @@ class _SplashScreenState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '$greeting,', - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$greeting,', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], ), - const SizedBox(height: 4), + ), + const SizedBox(height: 4), + if (_isLoadingProfile) + _buildLoadingShimmer() + else Text( - _userName, + _userProfile?.username ?? _userName, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, ), - ], - ), + ], ), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: const Color(0xFFE8FE54).withOpacity(0.3), - shape: BoxShape.circle, - ), - child: Icon( - _getGreetingIcon(hour), - color: Colors.black, - size: 24, + GestureDetector( + onTap: _showProfileModal, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + shape: BoxShape.circle, + border: Border.all( + color: Colors.blue.shade100, + width: 2, + ), + ), + child: const Icon( + Icons.person_outline, + color: Colors.blue, + size: 24, + ), ), ), ], ), const SizedBox(height: 16), Container( + width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.white, @@ -281,7 +272,7 @@ class _SplashScreenState extends State ), ), child: Row( - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.local_fire_department, @@ -305,20 +296,174 @@ class _SplashScreenState extends State ); } - IconData _getGreetingIcon(int hour) { - if (hour < 6) { - return Icons.bedtime; // Night time - } else if (hour < 12) { - return Icons.wb_sunny; // Morning + void _showProfileModal() { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + shape: BoxShape.circle, + ), + child: Icon( + Icons.person, + size: 32, + color: Colors.blue.shade700, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _userProfile?.username ?? _userName, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + if (_userProfile?.email != null) + Text( + _userProfile!.email, + style: TextStyle( + color: Colors.grey[600], + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + if (_userProfile != null) ...[ + _buildProfileInfoRow( + icon: Icons.height, + label: 'Height', + value: '${_userProfile!.height} cm', + ), + const SizedBox(height: 16), + _buildProfileInfoRow( + icon: Icons.monitor_weight_outlined, + label: 'Weight', + value: '${_userProfile!.weight} kg', + ), + ], + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: TextButton.icon( + onPressed: _handleLogout, + icon: const Icon(Icons.logout, color: Colors.red), + label: const Text( + 'Logout', + style: TextStyle(color: Colors.red), + ), + style: TextButton.styleFrom( + padding: const EdgeInsets.all(16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: Colors.red), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildProfileInfoRow({ + required IconData icon, + required String label, + required String value, + }) { + return Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 20, color: Colors.grey[700]), + ), + const SizedBox(width: 12), + Text( + label, + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ); + } + + Widget _buildLoadingShimmer() { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 150, + height: 24, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 8), + Container( + width: 100, + height: 16, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ); + } + + String _getGreeting(int hour) { + if (hour < 12) { + return 'Good morning'; } else if (hour < 17) { - return Icons.wb_cloudy; // Afternoon - } else if (hour < 20) { - return Icons.wb_twilight; // Evening + return 'Good afternoon'; } else { - return Icons.nightlight; // Night + return 'Good evening'; } } + String get _userName { + final user = context.read().userInfo; + return user?.username ?? ''; + } + Widget _buildQuickStats() { final totalWorkouts = _workoutInfo.length; final totalCalories = _workoutInfo.fold( @@ -525,18 +670,6 @@ class _SplashScreenState extends State letterSpacing: 1.2, ), ), - TextButton.icon( - onPressed: _handleLogout, - icon: const Icon(Icons.logout), - label: const Text('Logout'), - style: TextButton.styleFrom( - foregroundColor: Colors.black54, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), ], ); } @@ -603,7 +736,7 @@ class _SplashScreenState extends State mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( - 'Your Progress', + '', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, @@ -623,7 +756,7 @@ class _SplashScreenState extends State const SizedBox(height: 16), ProgressTracker( workouts: _workoutInfo, - isLoading: false, // We handle loading state in tab view + isLoading: _isLoading, error: _error, onRetry: _loadWorkoutInfo, ), diff --git a/lib/widgets/progress_tracker.dart b/lib/widgets/progress_tracker.dart index de4289f..0c09929 100644 --- a/lib/widgets/progress_tracker.dart +++ b/lib/widgets/progress_tracker.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'dart:math' show min, max; +import 'dart:math' as math; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; @@ -8,6 +8,8 @@ import 'package:workout_ai/models/progress_data.dart' as progress; import 'package:workout_ai/services/progress_service.dart'; import 'package:workout_ai/services/workout_info_service.dart'; +enum ChartView { weight, calories } + class ProgressTracker extends StatefulWidget { final List workouts; final bool isLoading; @@ -30,11 +32,16 @@ class _ProgressTrackerState extends State { progress.ProgressResponse? _progressData; bool _isLoadingProgress = true; Timer? _refreshTimer; + ChartView _currentView = ChartView.weight; @override void initState() { super.initState(); _loadProgressData(); + _setupRefreshTimer(); + } + + void _setupRefreshTimer() { _refreshTimer = Timer.periodic( const Duration(seconds: 30), (_) => _loadProgressData(), @@ -47,160 +54,6 @@ class _ProgressTrackerState extends State { super.dispose(); } - Widget _buildShimmerContent() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildShimmerWeightCard(), - const SizedBox(height: 20), - ...List.generate( - 3, - (index) => Padding( - padding: const EdgeInsets.only(bottom: 16), - child: _buildShimmerWorkoutItem(), - )), - const SizedBox(height: 20), - ], - ); - } - - Widget _buildShimmerWorkoutItem() { - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.05), - spreadRadius: 1, - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Shimmer.fromColors( - baseColor: Colors.grey[300]!, - highlightColor: Colors.grey[100]!, - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: double.infinity, - height: 16, - color: Colors.white, - ), - const SizedBox(height: 8), - Row( - children: [ - Container( - width: 60, - height: 24, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - ), - const SizedBox(width: 8), - Container( - width: 60, - height: 24, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildShimmerWeightCard() { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - spreadRadius: 1, - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Shimmer.fromColors( - baseColor: Colors.grey[300]!, - highlightColor: Colors.grey[100]!, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 120, - height: 16, - color: Colors.white, - ), - const SizedBox(height: 8), - Container( - width: 80, - height: 32, - color: Colors.white, - ), - const SizedBox(height: 24), - AspectRatio( - aspectRatio: 1.70, - child: Container( - color: Colors.white, - ), - ), - const SizedBox(height: 16), - Center( - child: Container( - width: 60, - height: 20, - color: Colors.white, - ), - ), - const SizedBox(height: 24), - Container( - width: 180, - height: 16, - color: Colors.white, - ), - const SizedBox(height: 8), - Container( - width: 100, - height: 32, - color: Colors.white, - ), - ], - ), - ), - ); - } - Future _loadProgressData() async { if (!mounted) return; @@ -221,6 +74,7 @@ class _ProgressTrackerState extends State { _isLoadingProgress = false; }); } catch (e) { + debugPrint('Error loading progress data: $e'); if (!mounted) return; setState(() { _isLoadingProgress = false; @@ -228,6 +82,12 @@ class _ProgressTrackerState extends State { } } + void _toggleChartView(ChartView view) { + setState(() { + _currentView = view; + }); + } + @override Widget build(BuildContext context) { final bool isLoading = widget.isLoading || _isLoadingProgress; @@ -241,6 +101,7 @@ class _ProgressTrackerState extends State { Future.delayed(const Duration(milliseconds: 500)), ]); }, + color: const Color(0xFFE8FE54), child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Padding( @@ -261,7 +122,6 @@ class _ProgressTrackerState extends State { : Colors.green; return _buildWorkoutItem(workout, color); }), - // Add bottom padding const SizedBox(height: 20), ], ), @@ -291,11 +151,350 @@ class _ProgressTrackerState extends State { final sortedData = List.from(_progressData!.data) ..sort((a, b) => a.month.compareTo(b.month)); - final weightChange = - sortedData.last.currentWeight - sortedData.first.currentWeight; - final percentChange = (weightChange / sortedData.first.currentWeight * 100); - final isGain = weightChange > 0; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Progress History', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => _toggleChartView(ChartView.weight), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _currentView == ChartView.weight + ? Colors.blue.shade50 + : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _currentView == ChartView.weight + ? Colors.blue.shade200 + : Colors.transparent, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.monitor_weight_outlined, + color: Colors.blue.shade400, size: 18), + const SizedBox(width: 8), + Text( + 'Current Weight', + style: TextStyle( + color: Colors.blue.shade700, fontSize: 14), + ), + ], + ), + const SizedBox(height: 8), + Text( + '${sortedData.last.currentWeight.toStringAsFixed(1)} kg', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + Text( + 'Avg: ${_progressData!.averageCurrentWeight} kg', + style: TextStyle( + color: Colors.blue.shade400, fontSize: 12), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () => _toggleChartView(ChartView.calories), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _currentView == ChartView.calories + ? Colors.orange.shade50 + : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _currentView == ChartView.calories + ? Colors.orange.shade200 + : Colors.transparent, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.local_fire_department, + color: Colors.orange.shade400, size: 18), + const SizedBox(width: 8), + Text( + 'Calories Burned', + style: TextStyle( + color: Colors.orange.shade700, fontSize: 14), + ), + ], + ), + const SizedBox(height: 8), + Text( + _progressData!.caloriesBurnIn3Month.toStringAsFixed(1), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.orange.shade700, + ), + ), + Text( + '3-Month Total', + style: TextStyle( + color: Colors.orange.shade400, fontSize: 12), + ), + ], + ), + ), + ), + ), + ], + ), + const SizedBox(height: 32), + SizedBox( + height: 250, + child: _buildChart(sortedData), + ), + ], + ); + } + + Widget _buildChart(List data) { + if (data.isEmpty) { + return const Center(child: Text('No data available')); + } + + try { + final MaterialColor mainColor = + _currentView == ChartView.weight ? Colors.blue : Colors.orange; + final bool isWeight = _currentView == ChartView.weight; + + double minY = isWeight ? double.infinity : 0; + double maxY = isWeight ? -double.infinity : 0; + + for (var item in data) { + if (isWeight) { + minY = math.min(minY, item.currentWeight); + maxY = math.max(maxY, item.currentWeight); + } else { + maxY = math.max(maxY, item.totalCalories); + } + } + + if (isWeight && minY.isFinite && maxY.isFinite) { + final padding = (maxY - minY) * 0.1; + minY -= padding; + maxY += padding; + } else if (!isWeight) { + maxY += maxY * 0.1; + } + + if (!minY.isFinite || !maxY.isFinite || minY >= maxY) { + minY = isWeight ? 0 : 0; + maxY = isWeight ? 100 : 1000; + } + + final interval = _calculateInterval(minY, maxY); + + return LineChart( + LineChartData( + backgroundColor: mainColor.shade50.withOpacity(0.3), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: interval, + getDrawingHorizontalLine: (value) => const FlLine( + color: Colors.white, + strokeWidth: 1, + ), + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + if (index < 0 || index >= data.length) { + return const SizedBox.shrink(); + } + final item = data[index]; + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + '${_getMonthName(item.month)}/${item.year}', + style: const TextStyle( + color: Colors.black54, + fontSize: 12, + ), + ), + ); + }, + interval: 1, + reservedSize: 35, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + return Text( + isWeight + ? value.toStringAsFixed(1) + : value.toInt().toString(), + style: TextStyle( + color: mainColor.shade400, + fontSize: 12, + ), + ); + }, + interval: interval, + reservedSize: 40, + ), + ), + ), + borderData: FlBorderData(show: false), + minY: minY, + maxY: maxY, + lineBarsData: [ + LineChartBarData( + spots: data.asMap().entries.map((entry) { + return FlSpot( + entry.key.toDouble(), + isWeight + ? entry.value.currentWeight + : entry.value.totalCalories, + ); + }).toList(), + isCurved: !isWeight, + curveSmoothness: 0.3, + color: mainColor.shade400, + barWidth: 3, + isStrokeCapRound: true, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) => + FlDotCirclePainter( + radius: 5, + color: Colors.white, + strokeWidth: 2, + strokeColor: mainColor.shade400, + ), + ), + belowBarData: BarAreaData( + show: true, + color: mainColor.shade50.withOpacity(0.3), + ), + ), + ], + ), + ); + } catch (e) { + debugPrint('Error building chart: $e'); + return Container( + height: 250, + alignment: Alignment.center, + child: Text('Error displaying chart: ${e.toString()}'), + ); + } + } + + double _calculateInterval(double min, double max) { + final range = max - min; + if (range <= 0) return 1; + + final rough = range / 5; + final magnitude = math.pow(10, (math.log(rough) / math.ln10).floor()); + final normalized = rough / magnitude; + + if (normalized < 1.5) return magnitude.toDouble(); + if (normalized < 3) return 2 * magnitude.toDouble(); + if (normalized < 7) return 5 * magnitude.toDouble(); + return 10 * magnitude.toDouble(); + } + + Widget _buildShimmerContent() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + width: 160, + height: 28, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: _buildShimmerCard(), + ), + const SizedBox(width: 12), + Expanded( + child: _buildShimmerCard(), + ), + ], + ), + const SizedBox(height: 32), + _buildShimmerChart(), + const SizedBox(height: 20), + ...List.generate( + 3, + (index) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _buildShimmerWorkoutItem(), + ), + ), + ], + ); + } + Widget _buildShimmerCard() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -310,253 +509,221 @@ class _ProgressTrackerState extends State { ), ], ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _buildHeaderSection(isGain, weightChange, percentChange), - const SizedBox(height: 24), - _buildChart(sortedData), - const SizedBox(height: 8), - _buildChartLegend(), - const SizedBox(height: 20), - _buildCaloriesSection(sortedData), - ], + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 100, + height: 16, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 12), + Container( + width: 80, + height: 24, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 8), + Container( + width: 60, + height: 12, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), ), ); } - Widget _buildHeaderSection( - bool isGain, double weightChange, double percentChange) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget _buildShimmerChart() { + return Container( + height: 250, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ); + } + + Widget _buildShimmerWorkoutItem() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.05), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Row( children: [ - Text( - 'Current Weight', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 14, + Container( + width: 36, + height: 36, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, ), ), - const SizedBox(height: 4), - Row( - children: [ - Text( - _progressData!.data.last.currentWeight.toStringAsFixed(1), - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.grey.shade800, + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + height: 16, + color: Colors.white, ), - ), - const SizedBox(width: 4), - Text( - 'Kg', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 14, + const SizedBox(height: 8), + Row( + children: [ + Container( + width: 60, + height: 24, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(width: 8), + Container( + width: 60, + height: 24, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + ), + ], ), - ), - ], + ], + ), ), ], ), - if (_progressData!.data.length > 1) + ), + ); + } + + Widget _buildWorkoutItem(WorkoutInfo workout, Color color) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.1)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: isGain ? Colors.red.shade50 : Colors.green.shade50, - borderRadius: BorderRadius.circular(8), + color: color.withOpacity(0.1), + shape: BoxShape.circle, ), - child: Row( + child: Icon( + workout.woName.contains('Push') + ? Icons.fitness_center + : Icons.accessibility_new, + color: color, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - Icon( - isGain ? Icons.arrow_upward : Icons.arrow_downward, - size: 16, - color: isGain ? Colors.red.shade600 : Colors.green.shade600, - ), - const SizedBox(width: 4), Text( - '${weightChange.abs().toStringAsFixed(1)} (${isGain ? '+' : '-'}${percentChange.toStringAsFixed(1)}%)', - style: TextStyle( - color: isGain ? Colors.red.shade600 : Colors.green.shade600, - fontSize: 14, - fontWeight: FontWeight.w500, + workout.woName, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, ), ), + const SizedBox(height: 8), + Row( + children: [ + _buildStatChip( + '${workout.sumWo} reps', + color, + color.withOpacity(0.1), + ), + const SizedBox(width: 8), + _buildStatChip( + '${workout.totalCalories.toStringAsFixed(1)} cal', + Colors.green, + Colors.green.withOpacity(0.1), + ), + ], + ), ], ), ), - ], - ); - } - - Widget _buildChartLegend() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildLegendItem('Weight', Colors.blue), - if (_progressData != null && _progressData!.data.length > 1) ...[ - const SizedBox(width: 24), - _buildLegendItem('Calories', Colors.green), ], - ], + ), ); } - Widget _buildLegendItem(String label, Color color) { - return Row( - children: [ - Container( - width: 12, - height: 12, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 6), - Text( - label, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - ), + Widget _buildStatChip(String text, Color textColor, Color bgColor) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + text, + style: TextStyle( + color: textColor, + fontWeight: FontWeight.w500, + fontSize: 12, ), - ], + ), ); } - Widget _buildChart(List sortedData) { - if (sortedData.isEmpty) return const SizedBox.shrink(); - - try { - final weights = sortedData.map((d) => d.currentWeight).toList(); - if (weights.isEmpty) return const SizedBox.shrink(); - - final minWeight = weights.reduce(min); - final maxWeight = weights.reduce(max); - - if (maxWeight - minWeight < 0.01) { - return Container( - height: 200, - alignment: Alignment.center, - child: Text( - 'Weight: ${weights.first.toStringAsFixed(1)} kg', - style: const TextStyle(fontSize: 16), - ), - ); - } - - return RepaintBoundary( - child: AspectRatio( - aspectRatio: 1.70, - child: Padding( - padding: - const EdgeInsets.only(right: 18, left: 12, top: 24, bottom: 12), - child: LineChart( - LineChartData( - gridData: FlGridData( - show: true, - drawVerticalLine: false, - horizontalInterval: (maxWeight - minWeight) / 4, - getDrawingHorizontalLine: (value) => FlLine( - color: Colors.grey.shade200, - strokeWidth: 1, - ), - ), - titlesData: FlTitlesData( - show: true, - rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 30, - interval: 1, - getTitlesWidget: (value, meta) { - final index = value.toInt(); - if (index < 0 || index >= sortedData.length) { - return const SizedBox.shrink(); - } - return Text( - _getMonthName(sortedData[index].month), - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 12, - ), - ); - }, - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - interval: (maxWeight - minWeight) / 4, - getTitlesWidget: (value, meta) { - return Text( - value.toStringAsFixed(1), - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 12, - ), - ); - }, - ), - ), - ), - borderData: FlBorderData(show: false), - minX: 0, - maxX: (sortedData.length - 1).toDouble(), - minY: minWeight - 1, - maxY: maxWeight + 1, - lineBarsData: [ - LineChartBarData( - spots: sortedData.asMap().entries.map((entry) { - return FlSpot( - entry.key.toDouble(), - entry.value.currentWeight, - ); - }).toList(), - isCurved: false, - color: Colors.blue, - barWidth: 3, - isStrokeCapRound: true, - dotData: FlDotData( - show: true, - getDotPainter: (spot, percent, barData, index) { - return FlDotCirclePainter( - radius: 4, - color: Colors.white, - strokeWidth: 2, - strokeColor: Colors.blue, - ); - }, - ), - ), - ], - ), - ), - ), - ), - ); - } catch (e) { - debugPrint('Chart error: $e'); - return Container( - height: 200, - alignment: Alignment.center, - child: const Text('Unable to display chart'), - ); - } - } - Widget _buildEmptyState() { return Container( width: double.infinity, @@ -565,6 +732,13 @@ class _ProgressTrackerState extends State { color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), child: Column( mainAxisSize: MainAxisSize.min, @@ -597,137 +771,6 @@ class _ProgressTrackerState extends State { ); } - Widget _buildWorkoutItem(WorkoutInfo workout, Color color) { - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Container( - decoration: BoxDecoration( - color: color.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.1)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.05), - spreadRadius: 1, - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Icon( - workout.woName.contains('Push') - ? Icons.fitness_center - : Icons.accessibility_new, - color: color, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - workout.woName, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - _buildStatChip( - '${workout.sumWo} reps', - color, - color.withOpacity(0.1), - ), - const SizedBox(width: 8), - _buildStatChip( - '${workout.totalCalories.toStringAsFixed(1)} cal', - Colors.green, - Colors.green.withOpacity(0.1), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildStatChip(String text, Color textColor, Color bgColor) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - text, - style: TextStyle( - color: textColor, - fontWeight: FontWeight.w500, - ), - ), - ); - } - - Widget _buildCaloriesSection(List sortedData) { - final lastEntry = sortedData.last; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Daily Calories', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 14, - ), - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - lastEntry.totalCalories.toStringAsFixed(0), - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.grey.shade800, - ), - ), - const SizedBox(width: 4), - Text( - 'kcal', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 14, - ), - ), - ], - ), - ], - ); - } - String _getMonthName(int month) { const monthNames = [ 'Jan', @@ -743,6 +786,10 @@ class _ProgressTrackerState extends State { 'Nov', 'Dec' ]; + if (month < 1 || month > 12) { + debugPrint('Invalid month number: $month'); + return 'Invalid'; + } return monthNames[month - 1]; } }