diff --git a/lib/app/layouts/widgets/plan_sheet/calendar_time_dialog.dart b/lib/app/layouts/widgets/plan_sheet/calendar_time_dialog.dart index 85e254c..2da9bf5 100644 --- a/lib/app/layouts/widgets/plan_sheet/calendar_time_dialog.dart +++ b/lib/app/layouts/widgets/plan_sheet/calendar_time_dialog.dart @@ -3,12 +3,10 @@ import 'package:flutter/material.dart'; class CalendarTimeDialog extends StatefulWidget { final String initialStart; final String initialEnd; - final bool isGoogleCalendarEvent; const CalendarTimeDialog({ this.initialStart = '', this.initialEnd = '', - required this.isGoogleCalendarEvent, super.key, }); diff --git a/lib/app/layouts/widgets/plan_sheet_widget.dart b/lib/app/layouts/widgets/plan_sheet_widget.dart index f4f6774..1e74f86 100644 --- a/lib/app/layouts/widgets/plan_sheet_widget.dart +++ b/lib/app/layouts/widgets/plan_sheet_widget.dart @@ -6,8 +6,6 @@ import 'package:intl/intl.dart'; import '../../../components/clock_widget.dart'; import '../../../features/auth/bloc/auth_bloc.dart'; import '../../../features/plan/models/plan_model.dart'; -import '../../../services/api_service.dart'; -import '../../../services/calendar_service.dart'; class ShowBottomSheet extends StatefulWidget { const ShowBottomSheet({super.key}); @@ -23,7 +21,6 @@ class _ShowBottomSheetState extends State final List _subjectFocusNodes = []; final Map _subjectDurations = {}; - final List _calendarControllers = [ TextEditingController(text: 'Lunch'), TextEditingController(text: 'Dinner'), @@ -40,20 +37,11 @@ class _ShowBottomSheetState extends State double _dialogBreakSelectedHours = 0.0; double _dialogSessionSelectedHours = 0.0; - final Map> _calendarTimes = { 0: {'start_at': '12:30', 'end_at': '13:30'}, 1: {'start_at': '19:30', 'end_at': '20:30'}, 2: {'start_at': '23:30', 'end_at': '07:30'}, }; - bool _syncWithGoogleCalendar = false; - - - final Set _googleCalendarEvents = {}; - - - final CalendarService _calendarService = CalendarService(); - late AnimationController _fadeAnimationController; late AnimationController _slideAnimationController; @@ -70,7 +58,6 @@ class _ShowBottomSheetState extends State _breakTimeController = TextEditingController(text: 'Add break time'); _addSubject(); - _fadeAnimationController.forward(); _slideAnimationController.forward(); } @@ -85,26 +72,26 @@ class _ShowBottomSheetState extends State vsync: this, ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _fadeAnimationController, - curve: Curves.easeOutCubic, - )); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _fadeAnimationController, + curve: Curves.easeOutCubic, + ), + ); _slideAnimation = Tween( begin: const Offset(0, 0.3), end: Offset.zero, - ).animate(CurvedAnimation( - parent: _slideAnimationController, - curve: Curves.easeOutCubic, - )); + ).animate( + CurvedAnimation( + parent: _slideAnimationController, + curve: Curves.easeOutCubic, + ), + ); } @override void dispose() { - for (var controller in _subjectControllers) { controller.dispose(); } @@ -412,23 +399,22 @@ class _ShowBottomSheetState extends State Future _createPlan() async { if (!_formKey.currentState!.validate()) { - return; } final authState = context.read().state; - if (authState is! Authenticated) { + if (authState is! AuthAuthenticated) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('You must be logged in to create a plan.')), + content: Text('You must be logged in to create a plan.'), + ), ); } return; } try { - final firebaseUid = authState.user.uid; - final userUuid = await ApiService().getUserUUIDByFirebaseUid(firebaseUid); + final userUuid = authState.id; final calendar = []; _calendarControllers.asMap().forEach((idx, controller) { final title = controller.text.trim(); @@ -437,33 +423,42 @@ class _ShowBottomSheetState extends State final startAt = times['start_at']!; final endAt = times['end_at']!; calendar.add( - CalendarEntry(startAt: startAt, endAt: endAt, title: title)); + CalendarEntry(startAt: startAt, endAt: endAt, title: title), + ); } }); - final subjects = _subjectControllers - .asMap() - .entries - .where((entry) => entry.value.text.trim().isNotEmpty) - .map((entry) { - final idx = entry.key; - final controller = entry.value; - final durationHmm = _subjectDurations[idx] ?? 0.0; - final durationString = - formatDurationFromHmm(durationHmm, defaultText: '0m'); - - return SubjectEntry( - subject: controller.text, - duration: durationHmm > 0 ? durationString : null, - ); - }).toList(); + final subjects = + _subjectControllers + .asMap() + .entries + .where((entry) => entry.value.text.trim().isNotEmpty) + .map((entry) { + final idx = entry.key; + final controller = entry.value; + final durationHmm = _subjectDurations[idx] ?? 0.0; + final durationString = formatDurationFromHmm( + durationHmm, + defaultText: '0m', + ); + + return SubjectEntry( + subject: controller.text, + duration: durationHmm > 0 ? durationString : null, + ); + }) + .toList(); final plan = PlanModel( userUUID: userUuid, - session: - formatDurationFromHmm(_dialogSessionSelectedHours, defaultText: '0m'), - breakDuration: - formatDurationFromHmm(_dialogBreakSelectedHours, defaultText: '0m'), + session: formatDurationFromHmm( + _dialogSessionSelectedHours, + defaultText: '0m', + ), + breakDuration: formatDurationFromHmm( + _dialogBreakSelectedHours, + defaultText: '0m', + ), calendar: calendar, subjects: subjects, ); @@ -474,131 +469,9 @@ class _ShowBottomSheetState extends State } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to create plan: $e')), - ); - } - } - } - - - void _clearGoogleCalendarEvents() { - - final List googleEventIndices = _googleCalendarEvents.toList()..sort(); - for (int i = googleEventIndices.length - 1; i >= 0; i--) { - final idx = googleEventIndices[i]; - if (idx < _calendarControllers.length) { - _calendarControllers[idx].dispose(); - _calendarFocusNodes[idx].dispose(); - _calendarControllers.removeAt(idx); - _calendarFocusNodes.removeAt(idx); - _calendarTimes.remove(idx); - final Map> updatedTimes = {}; - _calendarTimes.forEach((key, value) { - if (key > idx) { - updatedTimes[key - 1] = value; - } else { - updatedTimes[key] = value; - } - }); - _calendarTimes.clear(); - _calendarTimes.addAll(updatedTimes); - } - } - _googleCalendarEvents.clear(); - setState(() {}); - } - - void _syncGoogleCalendar() async { - if (!mounted) return; - - try { - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ), - const SizedBox(width: 12), - const Text('Syncing with Google Calendar...'), - ], - ), - duration: const Duration(seconds: 2), - ), - ); - - - _clearGoogleCalendarEvents(); - - - final events = await _calendarService.getEvents(); - - if (!mounted) return; - - setState(() { - - for (var event in events) { - final startTime = event.start?.dateTime?.toLocal(); - final endTime = event.end?.dateTime?.toLocal(); - - if (startTime != null && endTime != null) { - final controller = TextEditingController( - text: event.summary ?? 'Untitled Event', - ); - final focusNode = FocusNode(); - - _calendarControllers.add(controller); - _calendarFocusNodes.add(focusNode); - - final newIndex = _calendarControllers.length - 1; - _calendarTimes[newIndex] = { - 'start_at': DateFormat('HH:mm').format(startTime), - 'end_at': DateFormat('HH:mm').format(endTime), - 'date': DateFormat('E, d MMM').format(startTime), - }; - _googleCalendarEvents.add(newIndex); - } - } - }); - - if (mounted) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Successfully synced ${events.length} events from Google Calendar'), - backgroundColor: Theme.of(context).colorScheme.primary, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ); - } - } catch (error) { - if (mounted) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to sync with Google Calendar: $error'), - backgroundColor: Theme.of(context).colorScheme.error, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ); - - - setState(() { - _syncWithGoogleCalendar = false; - }); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to create plan: $e'))); } } } @@ -611,9 +484,7 @@ class _ShowBottomSheetState extends State return Form( key: _formKey, child: Container( - decoration: const BoxDecoration( - color: Colors.transparent, - ), + decoration: const BoxDecoration(color: Colors.transparent), child: FadeTransition( opacity: _fadeAnimation, child: SlideTransition( @@ -634,7 +505,7 @@ class _ShowBottomSheetState extends State const SizedBox(height: 24), _buildCalendarSection(context, colors, textTheme), const SizedBox(height: 32), - _buildActionButtons(context, colors, textTheme) + _buildActionButtons(context, colors, textTheme), ], ), ), @@ -647,7 +518,11 @@ class _ShowBottomSheetState extends State ); } - Widget _buildHeader(BuildContext context, ColorScheme colors, TextTheme textTheme) { + Widget _buildHeader( + BuildContext context, + ColorScheme colors, + TextTheme textTheme, + ) { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( @@ -663,7 +538,6 @@ class _ShowBottomSheetState extends State ), child: Column( children: [ - Row( children: [ Container( @@ -708,15 +582,16 @@ class _ShowBottomSheetState extends State ); } - - Widget _buildSubjectsSection(BuildContext context, ColorScheme colors, TextTheme textTheme) { + Widget _buildSubjectsSection( + BuildContext context, + ColorScheme colors, + TextTheme textTheme, + ) { return Container( decoration: BoxDecoration( color: colors.surfaceContainerLow, borderRadius: BorderRadius.circular(20), - border: Border.all( - color: colors.outline.withValues(alpha: 0.08), - ), + border: Border.all(color: colors.outline.withValues(alpha: 0.08)), ), child: Padding( padding: const EdgeInsets.all(20), @@ -748,7 +623,10 @@ class _ShowBottomSheetState extends State ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: colors.secondary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), @@ -789,7 +667,10 @@ class _ShowBottomSheetState extends State label: const Text('Add Subject'), style: TextButton.styleFrom( foregroundColor: colors.primary, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), @@ -801,7 +682,10 @@ class _ShowBottomSheetState extends State padding: const EdgeInsets.only(top: 8, left: 4), child: Text( 'At least one subject is required', - style: textTheme.bodySmall?.copyWith(color: colors.error, fontWeight: FontWeight.w600), + style: textTheme.bodySmall?.copyWith( + color: colors.error, + fontWeight: FontWeight.w600, + ), ), ), ], @@ -810,14 +694,17 @@ class _ShowBottomSheetState extends State ); } - Widget _buildSubjectItem(BuildContext context, int index, ColorScheme colors, TextTheme textTheme) { + Widget _buildSubjectItem( + BuildContext context, + int index, + ColorScheme colors, + TextTheme textTheme, + ) { return Container( decoration: BoxDecoration( color: colors.surfaceContainerHigh.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(16), - border: Border.all( - color: colors.outline.withValues(alpha: 0.1), - ), + border: Border.all(color: colors.outline.withValues(alpha: 0.1)), boxShadow: [ BoxShadow( color: colors.shadow.withValues(alpha: 0.05), @@ -928,7 +815,9 @@ class _ShowBottomSheetState extends State Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: colors.tertiaryContainer.withValues(alpha: 0.8), + color: colors.tertiaryContainer.withValues( + alpha: 0.8, + ), borderRadius: BorderRadius.circular(10), ), child: Icon( @@ -976,18 +865,20 @@ class _ShowBottomSheetState extends State ), ], ), - ) + ), ); } - Widget _buildTimingSection(BuildContext context, ColorScheme colors, TextTheme textTheme) { + Widget _buildTimingSection( + BuildContext context, + ColorScheme colors, + TextTheme textTheme, + ) { return Container( decoration: BoxDecoration( color: colors.surfaceContainerLow, borderRadius: BorderRadius.circular(20), - border: Border.all( - color: colors.outline.withValues(alpha: 0.08), - ), + border: Border.all(color: colors.outline.withValues(alpha: 0.08)), ), child: Padding( padding: const EdgeInsets.all(20), @@ -1068,9 +959,7 @@ class _ShowBottomSheetState extends State decoration: BoxDecoration( color: colors.surfaceContainerHigh.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(16), - border: Border.all( - color: colors.outline.withValues(alpha: 0.1), - ), + border: Border.all(color: colors.outline.withValues(alpha: 0.1)), ), child: Material( color: Colors.transparent, @@ -1088,11 +977,7 @@ class _ShowBottomSheetState extends State color: iconColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), - child: Icon( - icon, - color: iconColor, - size: 20, - ), + child: Icon(icon, color: iconColor, size: 20), ), const SizedBox(height: 8), Text( @@ -1104,7 +989,8 @@ class _ShowBottomSheetState extends State ), const SizedBox(height: 4), Text( - controller.text == 'Add session time' || controller.text == 'Add break time' + controller.text == 'Add session time' || + controller.text == 'Add break time' ? 'Set time' : controller.text, style: textTheme.titleMedium?.copyWith( @@ -1117,7 +1003,10 @@ class _ShowBottomSheetState extends State padding: const EdgeInsets.only(top: 6), child: Text( 'Session time must be > 0', - style: textTheme.bodySmall?.copyWith(color: colors.error, fontWeight: FontWeight.w600), + style: textTheme.bodySmall?.copyWith( + color: colors.error, + fontWeight: FontWeight.w600, + ), ), ), if (label == 'Break Time' && (_dialogBreakSelectedHours <= 0)) @@ -1125,25 +1014,30 @@ class _ShowBottomSheetState extends State padding: const EdgeInsets.only(top: 6), child: Text( 'Break time must be > 0', - style: textTheme.bodySmall?.copyWith(color: colors.error, fontWeight: FontWeight.w600), + style: textTheme.bodySmall?.copyWith( + color: colors.error, + fontWeight: FontWeight.w600, + ), ), ), ], ), ), ), - ) + ), ); } - Widget _buildCalendarSection(BuildContext context, ColorScheme colors, TextTheme textTheme) { + Widget _buildCalendarSection( + BuildContext context, + ColorScheme colors, + TextTheme textTheme, + ) { return Container( decoration: BoxDecoration( color: colors.surfaceContainerLow, borderRadius: BorderRadius.circular(20), - border: Border.all( - color: colors.outline.withValues(alpha: 0.08), - ), + border: Border.all(color: colors.outline.withValues(alpha: 0.08)), ), child: Padding( padding: const EdgeInsets.all(20), @@ -1175,7 +1069,10 @@ class _ShowBottomSheetState extends State ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: colors.tertiary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), @@ -1203,32 +1100,9 @@ class _ShowBottomSheetState extends State padding: const EdgeInsets.all(12), child: Row( children: [ - Icon( - Icons.sync_rounded, - color: colors.primary, - size: 16, - ), + Icon(Icons.sync_rounded, color: colors.primary, size: 16), const SizedBox(width: 8), - Expanded( - child: Text( - 'Sync with Google Calendar', - style: textTheme.bodySmall?.copyWith( - color: colors.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - Switch( - value: _syncWithGoogleCalendar, - onChanged: (value) { - setState(() { - _syncWithGoogleCalendar = value; - }); - if (value) { - _syncGoogleCalendar(); - } - }, - ), + Expanded(child: Container()), ], ), ), @@ -1259,7 +1133,10 @@ class _ShowBottomSheetState extends State label: const Text('Add Custom Event'), style: TextButton.styleFrom( foregroundColor: colors.tertiary, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), @@ -1272,11 +1149,14 @@ class _ShowBottomSheetState extends State ); } - Widget _buildCalendarItem(BuildContext context, int index, ColorScheme colors, TextTheme textTheme) { - final isGoogleEvent = _googleCalendarEvents.contains(index); + Widget _buildCalendarItem( + BuildContext context, + int index, + ColorScheme colors, + TextTheme textTheme, + ) { final isFixedEvent = index < 3; - IconData? fixedIcon; Color? fixedColor; String? fixedLabel; @@ -1302,226 +1182,237 @@ class _ShowBottomSheetState extends State return Container( decoration: BoxDecoration( - gradient: isFixedEvent - ? LinearGradient( - colors: [ - fixedColor!.withValues(alpha: 0.12), - colors.surfaceContainerHigh.withValues(alpha: 0.6), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ) - : null, - color: !isFixedEvent - ? colors.surfaceContainerHigh.withValues(alpha: 0.6) - : null, + gradient: + isFixedEvent + ? LinearGradient( + colors: [ + fixedColor!.withValues(alpha: 0.12), + colors.surfaceContainerHigh.withValues(alpha: 0.6), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: + !isFixedEvent + ? colors.surfaceContainerHigh.withValues(alpha: 0.6) + : null, borderRadius: BorderRadius.circular(18), border: Border.all( - color: isGoogleEvent - ? colors.primary.withValues(alpha: 0.3) - : isFixedEvent + color: + isFixedEvent ? fixedColor!.withValues(alpha: 0.3) : colors.outline.withValues(alpha: 0.1), - width: isGoogleEvent || isFixedEvent ? 1.8 : 1, + width: isFixedEvent ? 1.8 : 1, ), - boxShadow: isFixedEvent - ? [ - BoxShadow( - color: fixedColor!.withValues(alpha: 0.10), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ] - : [], + boxShadow: + isFixedEvent + ? [ + BoxShadow( + color: fixedColor!.withValues(alpha: 0.10), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ] + : [], ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), - child: isFixedEvent - ? Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: fixedColor!.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), + child: + isFixedEvent + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: fixedColor!.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(fixedIcon, color: fixedColor, size: 28), ), - child: Icon(fixedIcon, color: fixedColor, size: 28), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - fixedLabel!, - style: textTheme.titleMedium?.copyWith( - color: fixedColor, - fontWeight: FontWeight.bold, + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + fixedLabel!, + style: textTheme.titleMedium?.copyWith( + color: fixedColor, + fontWeight: FontWeight.bold, + ), ), ), - ), - IconButton( - icon: const Icon(Icons.delete_outline_rounded), - color: colors.error, - tooltip: 'Rimuovi', - onPressed: () { - setState(() { - _calendarControllers.removeAt(index); - _calendarFocusNodes.removeAt(index); - _calendarTimes.remove(index); - for (int i = index + 1; i < 3; i++) { - if (_calendarTimes.containsKey(i)) { - _calendarTimes[i - 1] = _calendarTimes[i]!; - _calendarTimes.remove(i); + IconButton( + icon: const Icon(Icons.delete_outline_rounded), + color: colors.error, + tooltip: 'Rimuovi', + onPressed: () { + setState(() { + _calendarControllers.removeAt(index); + _calendarFocusNodes.removeAt(index); + _calendarTimes.remove(index); + for (int i = index + 1; i < 3; i++) { + if (_calendarTimes.containsKey(i)) { + _calendarTimes[i - 1] = + _calendarTimes[i]!; + _calendarTimes.remove(i); + } } - } - }); - }, - ), - ], - ), - const SizedBox(height: 6), - Row( - children: [ - Text( - 'Time:', - style: textTheme.labelMedium?.copyWith( - color: colors.onSurfaceVariant, + }); + }, ), - ), - const SizedBox(width: 8), - Text( - '${_calendarTimes[index]?['start_at']} - ${_calendarTimes[index]?['end_at']}', - style: textTheme.bodyMedium?.copyWith( - color: fixedColor, - fontWeight: FontWeight.w600, + ], + ), + const SizedBox(height: 6), + Row( + children: [ + Text( + 'Time:', + style: textTheme.labelMedium?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + const SizedBox(width: 8), + Text( + '${_calendarTimes[index]?['start_at']} - ${_calendarTimes[index]?['end_at']}', + style: textTheme.bodyMedium?.copyWith( + color: fixedColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: () => _editCalendarTime(context, index), + icon: const Icon(Icons.edit_rounded, size: 18), + label: const Text('Edit Time'), + style: OutlinedButton.styleFrom( + foregroundColor: fixedColor, + side: BorderSide(color: fixedColor), + textStyle: textTheme.labelLarge, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, ), ), - ], - ), - const SizedBox(height: 8), - OutlinedButton.icon( - onPressed: () => _editCalendarTime(context, index), - icon: const Icon(Icons.edit_rounded, size: 18), - label: const Text('Edit Time'), - style: OutlinedButton.styleFrom( - foregroundColor: fixedColor, - side: BorderSide(color: fixedColor), - textStyle: textTheme.labelLarge, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), ), - ), - ], - ), - ), - ], - ) - : Row( - children: [ - Container( - padding: const EdgeInsets.all(7), - decoration: BoxDecoration( - color: isGoogleEvent - ? colors.primaryContainer - : isFixedEvent - ? fixedColor!.withValues(alpha: 0.18) - : colors.errorContainer.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - isGoogleEvent - ? Icons.sync_rounded - : isFixedEvent - ? fixedIcon - : Icons.event_rounded, - color: isGoogleEvent - ? colors.primary - : isFixedEvent - ? fixedColor - : colors.error, - size: 18, + ], + ), ), - ), - const SizedBox(width: 14), - Expanded( - child: TextFormField( - controller: _calendarControllers[index], - focusNode: _calendarFocusNodes[index], - enabled: !isGoogleEvent && !isFixedEvent, - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) { - if (!isFixedEvent && !isGoogleEvent && (value == null || value.trim().isEmpty)) { - return 'Event name cannot be empty'; - } - return null; - }, - decoration: InputDecoration( - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - hintText: isFixedEvent ? fixedLabel : null, - hintStyle: textTheme.bodyLarge?.copyWith(color: fixedColor?.withValues(alpha: 0.7)), - errorStyle: textTheme.bodySmall?.copyWith(color: colors.error, fontWeight: FontWeight.w600), + ], + ) + : Row( + children: [ + Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + color: + isFixedEvent + ? fixedColor!.withValues(alpha: 0.18) + : colors.errorContainer.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(10), ), - style: textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w700, - color: isGoogleEvent - ? colors.onSurfaceVariant - : isFixedEvent - ? fixedColor - : colors.onSurface, + child: Icon( + isFixedEvent ? fixedIcon : Icons.event_rounded, + color: isFixedEvent ? fixedColor : colors.error, + size: 18, ), ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: isFixedEvent ? fixedColor!.withValues(alpha: 0.10) : colors.surfaceContainerHigh.withValues(alpha: 0.10), - borderRadius: BorderRadius.circular(8), + const SizedBox(width: 14), + Expanded( + child: TextFormField( + controller: _calendarControllers[index], + focusNode: _calendarFocusNodes[index], + enabled: !isFixedEvent, + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) { + if (!isFixedEvent && + (value == null || value.trim().isEmpty)) { + return 'Event name cannot be empty'; + } + return null; + }, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + hintText: isFixedEvent ? fixedLabel : null, + hintStyle: textTheme.bodyLarge?.copyWith( + color: fixedColor?.withValues(alpha: 0.7), + ), + errorStyle: textTheme.bodySmall?.copyWith( + color: colors.error, + fontWeight: FontWeight.w600, + ), + ), + style: textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w700, + color: isFixedEvent ? fixedColor : colors.onSurface, + ), + ), ), - child: Text( - '${_calendarTimes[index]?['start_at']} - ${_calendarTimes[index]?['end_at']}', - style: textTheme.bodySmall?.copyWith( - color: isFixedEvent ? fixedColor : colors.onSurfaceVariant, - fontWeight: FontWeight.w600, + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: + isFixedEvent + ? fixedColor!.withValues(alpha: 0.10) + : colors.surfaceContainerHigh.withValues( + alpha: 0.10, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${_calendarTimes[index]?['start_at']} - ${_calendarTimes[index]?['end_at']}', + style: textTheme.bodySmall?.copyWith( + color: + isFixedEvent + ? fixedColor + : colors.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), ), ), - ), - const SizedBox(width: 8), - IconButton( - onPressed: () => _removecalendarAt(index), - icon: const Icon(Icons.remove_circle_outline_rounded), - iconSize: 18, - color: colors.error, - padding: const EdgeInsets.all(4), - constraints: const BoxConstraints(), - tooltip: 'Delete', - ), - if (!isFixedEvent) + const SizedBox(width: 8), IconButton( - onPressed: () => _editCalendarTime(context, index), - icon: const Icon(Icons.edit_rounded), + onPressed: () => _removecalendarAt(index), + icon: const Icon(Icons.remove_circle_outline_rounded), iconSize: 18, - color: colors.onSurfaceVariant, + color: colors.error, padding: const EdgeInsets.all(4), constraints: const BoxConstraints(), + tooltip: 'Delete', ), - ], - ), + if (!isFixedEvent) + IconButton( + onPressed: () => _editCalendarTime(context, index), + icon: const Icon(Icons.edit_rounded), + iconSize: 18, + color: colors.onSurfaceVariant, + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), + ], + ), ), ); } - - - - void _editCalendarTime(BuildContext context, int index) { - final currentTimes = _calendarTimes[index] ?? {'start_at': '12:00', 'end_at': '13:00'}; + final currentTimes = + _calendarTimes[index] ?? {'start_at': '12:00', 'end_at': '13:00'}; showDialog( context: context, @@ -1542,7 +1433,9 @@ class _ShowBottomSheetState extends State color: Theme.of(context).colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(12), border: Border.all( - color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.2), + color: Theme.of( + context, + ).colorScheme.outline.withValues(alpha: 0.2), ), ), child: ListTile( @@ -1563,7 +1456,8 @@ class _ShowBottomSheetState extends State ); if (picked != null) { dialogSetState(() { - startTime = '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}'; + startTime = + '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}'; }); } }, @@ -1575,7 +1469,9 @@ class _ShowBottomSheetState extends State color: Theme.of(context).colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(12), border: Border.all( - color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.2), + color: Theme.of( + context, + ).colorScheme.outline.withValues(alpha: 0.2), ), ), child: ListTile( @@ -1596,7 +1492,8 @@ class _ShowBottomSheetState extends State ); if (picked != null) { dialogSetState(() { - endTime = '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}'; + endTime = + '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}'; }); } }, @@ -1629,8 +1526,13 @@ class _ShowBottomSheetState extends State ); } - Widget _buildActionButtons(BuildContext context, ColorScheme colors, TextTheme textTheme) { - final bool hasFormErrors = _formKey.currentState == null || !_formKey.currentState!.validate(); + Widget _buildActionButtons( + BuildContext context, + ColorScheme colors, + TextTheme textTheme, + ) { + final bool hasFormErrors = + _formKey.currentState == null || !_formKey.currentState!.validate(); return Row( children: [ Expanded( @@ -1638,9 +1540,7 @@ class _ShowBottomSheetState extends State decoration: BoxDecoration( color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(16), - border: Border.all( - color: colors.outline.withValues(alpha: 0.2), - ), + border: Border.all(color: colors.outline.withValues(alpha: 0.2)), ), child: TextButton.icon( onPressed: () => Navigator.of(context).pop(), @@ -1664,10 +1564,7 @@ class _ShowBottomSheetState extends State gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ - colors.primary, - colors.primary.withValues(alpha: 0.8), - ], + colors: [colors.primary, colors.primary.withValues(alpha: 0.8)], ), borderRadius: BorderRadius.circular(16), boxShadow: [ diff --git a/lib/app/router.dart b/lib/app/router.dart index f4df7ad..d2668b9 100644 --- a/lib/app/router.dart +++ b/lib/app/router.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ketchapp_flutter/app/pages/error_page.dart'; import 'package:ketchapp_flutter/app/layouts/main_layout.dart'; +import 'package:ketchapp_flutter/features/auth/bloc/auth_bloc.dart'; import 'package:ketchapp_flutter/features/home/bloc/home_bloc.dart'; import 'package:ketchapp_flutter/features/auth/presentation/pages/login_page.dart'; import 'package:ketchapp_flutter/features/auth/presentation/pages/register_page.dart'; @@ -11,35 +12,40 @@ import 'package:ketchapp_flutter/features/home/presentation/pages/home_page.dart import 'package:ketchapp_flutter/features/plan/models/plan_model.dart'; import 'package:ketchapp_flutter/features/plan/presentation/pages/plan_creation_loading_page.dart'; import 'package:ketchapp_flutter/features/rankings/presentation/ranking_page.dart'; -import 'package:ketchapp_flutter/features/statistics/bloc/statistics_bloc.dart'; + import 'package:ketchapp_flutter/features/timer/presentation/timer_page.dart'; import 'package:ketchapp_flutter/features/welcome/presentation/pages/welcome_page.dart'; import 'package:ketchapp_flutter/features/profile/presentation/pages/profile_page.dart'; -import '../features/auth/bloc/api_auth_bloc.dart'; + import '../features/statistics/presentation/statistics_page.dart'; -import 'package:ketchapp_flutter/features/auth/presentation/pages/forgot_password_page.dart'; import 'package:ketchapp_flutter/features/statistics/bloc/api_statistics_bloc.dart'; import 'package:ketchapp_flutter/features/statistics/bloc/statistics_event.dart'; import 'package:ketchapp_flutter/services/api_service.dart'; GoRouter createRouter(BuildContext context) { - final apiAuthBloc = context.read(); - return GoRouter( initialLocation: '/', - refreshListenable: GoRouterRefreshStream(apiAuthBloc.stream), redirect: (context, state) { - final authState = apiAuthBloc.state; - final loggedIn = authState is ApiAuthenticated; + final authState = context.read().state; + final isAuthenticated = authState is AuthAuthenticated; final location = state.matchedLocation; - final isAuthRoute = location == '/login' || location == '/register' || location == '/forgot_password'; + final isAuthRoute = + location == '/login' || + location == '/register' || + location == '/forgot_password'; final isWelcomeRoute = location == '/'; - if (!loggedIn) { - if (!isAuthRoute && !isWelcomeRoute) return '/'; - return null; + // Se NON autenticato e non sei su login/register/welcome, manda a login + if (!isAuthenticated && !isAuthRoute && !isWelcomeRoute) { + return '/login'; } - if (isAuthRoute || isWelcomeRoute) return '/home'; + + // Se autenticato e sei su login/register/welcome, manda a home + if (isAuthenticated && (isAuthRoute || isWelcomeRoute)) { + return '/home'; + } + + // Altrimenti nessun redirect return null; }, routes: [ @@ -49,10 +55,6 @@ GoRouter createRouter(BuildContext context) { path: '/register', builder: (context, state) => const RegisterPage(), ), - GoRoute( - path: '/forgot_password', - builder: (context, state) => const ForgotPasswordPage(), - ), GoRoute( path: '/plan-creation-loading', redirect: (context, state) { @@ -74,25 +76,27 @@ GoRouter createRouter(BuildContext context) { }, ), ShellRoute( - builder: (context, state, child) => BlocProvider( - create: (context) => HomeBloc(), - child: MainLayout(child: child), - ), + builder: + (context, state, child) => BlocProvider( + create: (context) => HomeBloc(), + child: MainLayout(child: child), + ), routes: [ GoRoute( - path: '/home', - builder: (context, state) { - final refresh = state.uri.queryParameters['refresh'] == 'true'; - return HomePage(refresh: refresh); - }), + path: '/home', + builder: (context, state) { + final refresh = state.uri.queryParameters['refresh'] == 'true'; + return HomePage(refresh: refresh); + }, + ), GoRoute( path: '/statistics', builder: (context, state) { return BlocProvider( - create: (context) => ApiStatisticsBloc( - apiAuthBloc: context.read(), - apiService: context.read(), - )..add(const StatisticsLoadRequested()), + create: + (context) => ApiStatisticsBloc( + apiService: context.read(), + )..add(const StatisticsLoadRequested()), child: StatisticsPage(), ); }, diff --git a/lib/features/auth/bloc/api_auth_bloc.dart b/lib/features/auth/bloc/api_auth_bloc.dart deleted file mode 100644 index 8e18c6c..0000000 --- a/lib/features/auth/bloc/api_auth_bloc.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:ketchapp_flutter/services/api_auth_service.dart'; -import 'package:ketchapp_flutter/services/api_exceptions.dart'; -import 'package:ketchapp_flutter/services/api_service.dart'; -import 'package:meta/meta.dart'; - -part 'api_auth_event.dart'; -part 'api_auth_state.dart'; - -class ApiAuthBloc extends Bloc { - final ApiAuthService _apiAuthService; - final ApiService _apiService; // Per gestire il token - - ApiAuthBloc({required ApiAuthService apiAuthService, required ApiService apiService}) - : _apiAuthService = apiAuthService, - _apiService = apiService, - super(ApiAuthInitial()) { - on((event, emit) async { - emit(ApiAuthLoading()); - try { - final isAuthenticated = await _apiAuthService.isAuthenticated(); - if (isAuthenticated) { - // Se il token è valido, potresti voler recuperare i dati dell'utente - final userData = await _apiService.fetchData('auth/me'); - emit(ApiAuthenticated(userData)); - } else { - emit(ApiUnauthenticated()); - } - } catch (_) { - emit(ApiUnauthenticated()); - } - }); - - on((event, emit) async { - emit(ApiAuthLoading()); - try { - final response = await _apiAuthService.login(event.username, event.password); - // Assumendo che la risposta contenga il token e i dati utente - final token = response['token']; - final user = response['user']; - - // Salva il token nel servizio API per le richieste future - _apiService.setAuthToken(token); - - emit(ApiAuthenticated(user)); - } on ApiException catch (e) { - emit(ApiAuthFailure(e.message)); - } catch (e) { - emit(ApiAuthFailure('Errore sconosciuto durante il login: ${e.toString()}')); - } - }); - - on((event, emit) async { - emit(ApiAuthLoading()); - try { - await _apiAuthService.register( - username: event.username, - email: event.email, - password: event.password, - ); - // Dopo la registrazione, potresti voler effettuare il login automaticamente - add(ApiAuthLoginRequested(event.email, event.password)); - } on ApiException catch (e) { - emit(ApiAuthFailure(e.message)); - } catch (e) { - emit(ApiAuthFailure('Errore sconosciuto durante la registrazione: ${e.toString()}')); - } - }); - - on((event, emit) async { - emit(ApiAuthLoading()); - try { - await _apiAuthService.logout(); - _apiService.clearAuthToken(); // Rimuovi il token - emit(ApiUnauthenticated()); - } on ApiException catch (e) { - emit(ApiAuthFailure(e.message)); - } catch (e) { - emit(ApiAuthFailure('Errore durante il logout: ${e.toString()}')); - } - }); - - on((event, emit) async { - emit(ApiAuthLoading()); - try { - await _apiAuthService.sendPasswordResetEmail(event.email); - emit(ApiAuthPasswordResetSuccess('Email di reset inviata a ${event.email}.')); - } on ApiException catch (e) { - emit(ApiAuthFailure(e.message)); - } catch (e) { - emit(ApiAuthFailure('Errore durante la richiesta di reset password: ${e.toString()}')); - } - }); - } -} diff --git a/lib/features/auth/bloc/api_auth_event.dart b/lib/features/auth/bloc/api_auth_event.dart deleted file mode 100644 index 952d316..0000000 --- a/lib/features/auth/bloc/api_auth_event.dart +++ /dev/null @@ -1,32 +0,0 @@ -part of 'api_auth_bloc.dart'; - -@immutable -abstract class ApiAuthEvent {} - -class ApiAuthCheckRequested extends ApiAuthEvent {} - -class ApiAuthLoginRequested extends ApiAuthEvent { - final String username; - final String password; - - ApiAuthLoginRequested(this.username, this.password); -} - -class ApiAuthRegisterRequested extends ApiAuthEvent { - final String username; - final String email; - final String password; - - ApiAuthRegisterRequested({ - required this.username, - required this.email, - required this.password, - }); -} - -class ApiAuthLogoutRequested extends ApiAuthEvent {} - -class ApiAuthPasswordResetRequested extends ApiAuthEvent { - final String email; - ApiAuthPasswordResetRequested(this.email); -} diff --git a/lib/features/auth/bloc/api_auth_state.dart b/lib/features/auth/bloc/api_auth_state.dart deleted file mode 100644 index f29a921..0000000 --- a/lib/features/auth/bloc/api_auth_state.dart +++ /dev/null @@ -1,27 +0,0 @@ -part of 'api_auth_bloc.dart'; - -@immutable -abstract class ApiAuthState {} - -class ApiAuthInitial extends ApiAuthState {} - -class ApiAuthLoading extends ApiAuthState {} - -class ApiAuthenticated extends ApiAuthState { - // Potresti voler salvare qui i dati dell'utente o il token - final Map userData; - ApiAuthenticated(this.userData); -} - -class ApiUnauthenticated extends ApiAuthState {} - -class ApiAuthFailure extends ApiAuthState { - final String error; - ApiAuthFailure(this.error); -} - -class ApiAuthPasswordResetSuccess extends ApiAuthState { - final String message; - ApiAuthPasswordResetSuccess(this.message); -} - diff --git a/lib/features/auth/bloc/auth_bloc.dart b/lib/features/auth/bloc/auth_bloc.dart index aaeeaeb..559d185 100644 --- a/lib/features/auth/bloc/auth_bloc.dart +++ b/lib/features/auth/bloc/auth_bloc.dart @@ -1,227 +1,63 @@ -import 'dart:async'; import 'package:bloc/bloc.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:flutter/material.dart'; -import 'package:google_sign_in/google_sign_in.dart'; -import 'package:googleapis/calendar/v3.dart' as cal; import 'package:ketchapp_flutter/services/api_service.dart'; import 'package:meta/meta.dart'; -import '../../../services/api_exceptions.dart'; part 'auth_event.dart'; part 'auth_state.dart'; -const String webClientId = "1049541862968-7fa3abk4ja0794u5822ou6h9hem1j2go.apps.googleusercontent.com"; - -const List calendarScopes = [ - cal.CalendarApi.calendarScope, -]; - class AuthBloc extends Bloc { - final FirebaseAuth _firebaseAuth; final ApiService _apiService; - final GoogleSignIn _googleSignIn; - StreamSubscription? _userSubscription; - - AuthBloc({ - required FirebaseAuth firebaseAuth, - required ApiService apiService, - }) : _firebaseAuth = firebaseAuth, - _apiService = apiService, - _googleSignIn = GoogleSignIn( - clientId: kIsWeb ? webClientId : null, - scopes: calendarScopes, - ), - super(AuthInitial()) { - _userSubscription = _firebaseAuth.authStateChanges().listen((user) { - add(_AuthUserChanged(user)); - }); - - on<_AuthUserChanged>((event, emit) async { - if (event.user != null) { - try { - final userUuid = await _apiService.getUserUUIDByFirebaseUid(event.user!.uid); - emit(Authenticated(event.user!, userUuid)); - } catch (e) { - emit(AuthError(e.toString())); - } - } else { - emit(Unauthenticated()); - } - }); + AuthBloc({required ApiService apiService}) + : _apiService = apiService, + super(AuthInitial()) { on((event, emit) async { - emit(AuthVerifying()); + emit(AuthLoading()); try { - String emailToLogin; - if (event.username.contains('@') && event.username.contains('.')) { - emailToLogin = event.username; - } else { - try { - final userData = await _apiService.findEmailByUsername(event.username); - if (userData is Map && userData['email'] != null) { - emailToLogin = userData['email'] as String; - } else if (userData is String) { - emailToLogin = userData; - } - else { - emit(const AuthError('Username non trovato o email non associata.')); - return; - } - } on NotFoundException { - emit(const AuthError('Username non trovato.')); - return; - } on ApiException catch (e) { - emit(AuthError('Errore nel recuperare l\'email per l\'username: ${e.message}')); - return; - } catch (e) { - emit(const AuthError('Errore sconosciuto nel recuperare l\'email per l\'username.')); - return; - } - } - - await _firebaseAuth.signInWithEmailAndPassword( - email: emailToLogin, - password: event.password, + final response = await _apiService.postData('auth/login', { + 'username': event.username, + 'password': event.password, + }); + await _apiService.setAuthToken(response['token']); + emit( + AuthAuthenticated( + response['id'], + response['username'], + response['email'], + response['token'], + ), ); - } on FirebaseAuthException catch (e) { - emit(AuthError(_mapAuthErrorCodeToMessage(e.code))); } catch (e) { - emit(const AuthError('Errore sconosciuto durante il login.')); + emit(AuthError('Login fallito: ${e.toString()}')); } }); on((event, emit) async { - emit(AuthVerifying()); - UserCredential? userCredential; - + emit(AuthLoading()); try { - userCredential = - await _firebaseAuth.createUserWithEmailAndPassword( - email: event.email, - password: event.password, - ); - - if (userCredential.user != null) { - await _apiService.postData('users', { - 'firebaseUid': userCredential.user!.uid, - 'email': event.email, - 'username': event.username, - }); - } else { - emit(const AuthError('Registrazione Firebase riuscita ma utente nullo.')); - } - } on FirebaseAuthException catch (e) { - emit(AuthError(_mapAuthErrorCodeToMessage(e.code))); - } on UsernameAlreadyExistsException catch (e) { - await userCredential?.user?.delete(); - emit(AuthError(e.message)); - } on EmailAlreadyExistsInBackendException catch (e) { - await userCredential?.user?.delete(); - emit(AuthError(e.message)); - } on ConflictException catch (e) { - await userCredential?.user?.delete(); - emit(AuthError(e.message)); - } on ApiException catch (e) { - await userCredential?.user?.delete(); - emit(AuthError( - 'Registrazione Firebase riuscita, ma errore del server: ${e.message}')); - } catch (e) { - await userCredential?.user?.delete(); - emit(AuthError('Errore sconosciuto durante la registrazione: ${e.toString()}')); - } - }); - - on((event, emit) async { - emit(AuthVerifying()); - try { - if (kIsWeb && _googleSignIn.clientId == null) { - } - - final GoogleSignInAccount? googleUser = await _googleSignIn.signIn(); - if (googleUser == null) { - emit(Unauthenticated()); - return; - } - - final GoogleSignInAuthentication googleAuth = await googleUser.authentication; - final AuthCredential credential = GoogleAuthProvider.credential( - accessToken: googleAuth.accessToken, - idToken: googleAuth.idToken, + final response = await _apiService.postData('auth/register', { + 'username': event.username, + 'email': event.email, + 'password': event.password, + }); + await _apiService.setAuthToken(response['token']); + emit( + AuthAuthenticated( + response['id'], + response['username'], + response['email'], + response['token'], + ), ); - - final UserCredential userCredential = await _firebaseAuth.signInWithCredential(credential); - - - if (userCredential.user != null && userCredential.additionalUserInfo?.isNewUser == true) { - try { - await _apiService.postData('users', { - 'firebaseUid': userCredential.user!.uid, - 'email': userCredential.user!.email, - 'username': userCredential.user!.displayName ?? userCredential.user!.email?.split('@')[0], - 'displayName': userCredential.user!.displayName, - }); - } on ApiException catch (e) { - await _firebaseAuth.signOut(); - await _googleSignIn.signOut(); - emit(AuthError('Login Google riuscito, ma errore registrazione backend: ${e.message}')); - return; - } - } - - } on FirebaseAuthException catch (e) { - emit(AuthError(_mapAuthErrorCodeToMessage(e.code))); - } on ApiException catch (e) { - emit(AuthError('Errore API durante il login con Google: ${e.message}')); } catch (e) { - emit(AuthError('Errore sconosciuto durante il login con Google: ${e.toString()}')); + emit(AuthError('Registrazione fallita: ${e.toString()}')); } }); on((event, emit) async { - emit(AuthVerifying()); - try { - await _googleSignIn.signOut(); - await _firebaseAuth.signOut(); - } catch (e) { - emit(const AuthError('Errore durante il logout.')); - } + emit(AuthLoading()); + await _apiService.clearAuthToken(); + emit(Unauthenticated()); }); - - on((event, emit) async { - emit(AuthVerifying()); - try { - await _firebaseAuth.sendPasswordResetEmail(email: event.email); - emit(AuthPasswordResetEmailSentSuccess('Email di reset inviata a ${event.email}. Controlla la tua casella di posta.')); - } on FirebaseAuthException catch (e) { - emit(AuthError(_mapAuthErrorCodeToMessage(e.code))); - } catch (e) { - emit(AuthError('Errore durante l\'invio dell\'email di reset: ${e.toString()}')); - } - }); - } - - String _mapAuthErrorCodeToMessage(String code) { - switch (code) { - case 'user-not-found': - case 'wrong-password': - case 'invalid-credential': - return 'Credenziali non valide.'; - case 'invalid-email': - return 'L\'indirizzo email non è valido.'; - case 'email-already-in-use': - return 'L\'account Firebase esiste già per questa email.'; - case 'weak-password': - return 'La password fornita è troppo debole.'; - default: - return 'Errore di autenticazione Firebase. Riprova.'; - } - } - - @override - Future close() { - _userSubscription?.cancel(); - return super.close(); } } diff --git a/lib/features/auth/bloc/auth_event.dart b/lib/features/auth/bloc/auth_event.dart index 9513a93..4189962 100644 --- a/lib/features/auth/bloc/auth_event.dart +++ b/lib/features/auth/bloc/auth_event.dart @@ -15,26 +15,16 @@ class AuthRegisterRequested extends AuthEvent { final String email; final String password; - AuthRegisterRequested({required this.username, required this.email, required this.password}); + AuthRegisterRequested({ + required this.username, + required this.email, + required this.password, + }); } -class AuthLogoutRequested extends AuthEvent { - final String? token; - AuthLogoutRequested({this.token}); - -} - -class AuthGoogleSignInRequested extends AuthEvent { - final String? token; - AuthGoogleSignInRequested({this.token}); -} - -class _AuthUserChanged extends AuthEvent { - final User? user; - _AuthUserChanged(this.user); -} +class AuthLogoutRequested extends AuthEvent {} class AuthPasswordResetRequested extends AuthEvent { final String email; AuthPasswordResetRequested({required this.email}); -} \ No newline at end of file +} diff --git a/lib/features/auth/bloc/auth_state.dart b/lib/features/auth/bloc/auth_state.dart index bcf75bd..aee0078 100644 --- a/lib/features/auth/bloc/auth_state.dart +++ b/lib/features/auth/bloc/auth_state.dart @@ -6,15 +6,21 @@ abstract class AuthState { } class AuthInitial extends AuthState {} + class AuthLoading extends AuthState {} + class AuthVerifying extends AuthState {} -class Authenticated extends AuthState { - final User user; - final String userUuid; - final bool isGoogleSignIn; - const Authenticated(this.user, this.userUuid, {this.isGoogleSignIn = false}); + +class AuthAuthenticated extends AuthState { + final String id; + final String username; + final String email; + final String token; + const AuthAuthenticated(this.id, this.username, this.email, this.token); } + class Unauthenticated extends AuthState {} + class AuthError extends AuthState { final String message; const AuthError(this.message); @@ -23,4 +29,4 @@ class AuthError extends AuthState { class AuthPasswordResetEmailSentSuccess extends AuthState { final String message; const AuthPasswordResetEmailSentSuccess(this.message); -} \ No newline at end of file +} diff --git a/lib/features/auth/presentation/pages/forgot_password_page.dart b/lib/features/auth/presentation/pages/forgot_password_page.dart deleted file mode 100644 index 01cc0f1..0000000 --- a/lib/features/auth/presentation/pages/forgot_password_page.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:ketchapp_flutter/features/auth/bloc/auth_bloc.dart'; -import 'forgot_password_shimmer_page.dart'; - -class ForgotPasswordPage extends StatefulWidget { - const ForgotPasswordPage({super.key}); - - @override - State createState() => _ForgotPasswordPageState(); -} - -class _ForgotPasswordPageState extends State { - final _formKey = GlobalKey(); - late final TextEditingController _emailController; - - bool _showShimmer = true; - - @override - void initState() { - super.initState(); - _emailController = TextEditingController(); - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - setState(() { - _showShimmer = false; - }); - } - }); - } - - @override - void dispose() { - _emailController.dispose(); - super.dispose(); - } - - void _submitForgotPassword() { - if (_formKey.currentState!.validate()) { - context - .read() - .add(AuthPasswordResetRequested(email: _emailController.text.trim())); - } - } - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - final isLoading = context.watch().state is AuthLoading; - if (_showShimmer || isLoading) { - return const ForgotPasswordShimmerPage(); - } - return Scaffold( - appBar: AppBar( - title: const Text('Reset Password'), - backgroundColor: colors.surface, - elevation: 0, - iconTheme: IconThemeData(color: colors.onSurface), - ), - backgroundColor: colors.surface, - body: SafeArea( - child: BlocListener( - listener: (context, state) { - if (state is AuthPasswordResetEmailSentSuccess) { - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: Colors.green, - ), - ); - } else if (state is AuthError) { - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: colors.error, - ), - ); - } - }, - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0), - child: Form( - key: _formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Icon( - Icons.lock_reset_outlined, - size: 60, - color: colors.primary, - ), - const SizedBox(height: 24), - Text( - 'Inserisci la tua email', - style: textTheme.headlineSmall?.copyWith(color: colors.onSurface), - textAlign: TextAlign.center, - ), - const SizedBox(height: 12), - Text( - 'Ti invieremo un link per resettare la tua password.', - style: textTheme.bodyMedium?.copyWith(color: colors.onSurfaceVariant), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - TextFormField( - controller: _emailController, - decoration: InputDecoration( - labelText: 'Email', - hintText: 'iltuoindirizzo@email.com', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.isEmpty || !value.contains('@')) { - return 'Inserisci un\'email valida.'; - } - return null; - }, - ), - const SizedBox(height: 24), - BlocBuilder( - builder: (context, state) { - final isLoading = state is AuthLoading; - return FilledButton( - onPressed: isLoading ? null : _submitForgotPassword, - style: FilledButton.styleFrom( - backgroundColor: colors.primary, - foregroundColor: colors.onPrimary, - minimumSize: const Size.fromHeight(48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - textStyle: textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - child: isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2.5, - color: Colors.white, - ), - ) - : const Text('Invia Email di Reset'), - ); - }, - ), - ], - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/features/auth/presentation/pages/forgot_password_shimmer_page.dart b/lib/features/auth/presentation/pages/forgot_password_shimmer_page.dart deleted file mode 100644 index 00c96a9..0000000 --- a/lib/features/auth/presentation/pages/forgot_password_shimmer_page.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.dart'; - -class ForgotPasswordShimmerPage extends StatelessWidget { - const ForgotPasswordShimmerPage({super.key}); - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - return Scaffold( - appBar: AppBar( - title: Container(height: 24, width: 120, color: Colors.white, margin: const EdgeInsets.symmetric(vertical: 8)), - backgroundColor: colors.surface, - elevation: 0, - iconTheme: IconThemeData(color: colors.onSurface), - ), - backgroundColor: colors.surface, - body: SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0), - child: Shimmer.fromColors( - baseColor: colors.surfaceVariant, - highlightColor: colors.surface, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - height: 60, - width: 60, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - ), - ), - const SizedBox(height: 24), - Container( - height: 28, - width: 180, - color: Colors.white, - margin: const EdgeInsets.symmetric(vertical: 8), - ), - const SizedBox(height: 12), - Container( - height: 20, - width: 220, - color: Colors.white, - margin: const EdgeInsets.symmetric(vertical: 8), - ), - const SizedBox(height: 32), - Container( - height: 56, - width: double.infinity, - color: Colors.white, - margin: const EdgeInsets.symmetric(vertical: 8), - ), - const SizedBox(height: 24), - Container( - height: 48, - width: double.infinity, - color: Colors.white, - margin: const EdgeInsets.symmetric(vertical: 8), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index 6452665..7a1a9d3 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ketchapp_flutter/features/auth/bloc/auth_bloc.dart'; + import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; -import 'package:ketchapp_flutter/features/auth/bloc/api_auth_bloc.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'login_shrimmer_page.dart'; class LoginPage extends StatefulWidget { @@ -11,7 +12,7 @@ class LoginPage extends StatefulWidget { @override State createState() => _LoginPageState(); } - + class _LoginPageState extends State with TickerProviderStateMixin { late AnimationController _fadeAnimationController; late AnimationController _scaleAnimationController; @@ -46,29 +47,47 @@ class _LoginPageState extends State with TickerProviderStateMixin { final SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle( statusBarColor: Colors.transparent, - statusBarIconBrightness: colors.brightness == Brightness.light - ? Brightness.dark - : Brightness.light, + statusBarIconBrightness: + colors.brightness == Brightness.light + ? Brightness.dark + : Brightness.light, statusBarBrightness: colors.brightness, systemNavigationBarColor: colors.surface, - systemNavigationBarIconBrightness: colors.brightness == Brightness.light - ? Brightness.dark - : Brightness.light, + systemNavigationBarIconBrightness: + colors.brightness == Brightness.light + ? Brightness.dark + : Brightness.light, ); return AnnotatedRegion( value: systemUiOverlayStyle, child: Scaffold( backgroundColor: colors.surface, - body: BlocListener( + body: BlocListener( listener: (context, state) { - if (state is ApiAuthFailure) { + if (state is AuthAuthenticated) { + if (!mounted) return; + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text('Login effettuato con successo!'), + backgroundColor: colors.primary, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ); + context.go('/home'); + } + if (state is AuthError) { if (!mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.error), + content: Text(state.message), backgroundColor: colors.error, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( @@ -131,21 +150,19 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { vsync: this, ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _fadeAnimationController, - curve: Curves.easeOutCubic, - )); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _fadeAnimationController, + curve: Curves.easeOutCubic, + ), + ); - _scaleAnimation = Tween( - begin: 0.95, - end: 1.0, - ).animate(CurvedAnimation( - parent: _scaleAnimationController, - curve: Curves.easeOutBack, - )); + _scaleAnimation = Tween(begin: 0.95, end: 1.0).animate( + CurvedAnimation( + parent: _scaleAnimationController, + curve: Curves.easeOutBack, + ), + ); } @override @@ -161,12 +178,12 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { HapticFeedback.lightImpact(); FocusScope.of(context).unfocus(); if (_formKey.currentState!.validate()) { - context.read().add( - ApiAuthLoginRequested( - _usernameController.text.trim(), - _passwordController.text, - ), - ); + context.read().add( + AuthLoginRequested( + username: _usernameController.text.trim(), + password: _passwordController.text, + ), + ); } } @@ -175,7 +192,7 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { final ColorScheme colors = Theme.of(context).colorScheme; final TextTheme textTheme = Theme.of(context).textTheme; final Size size = MediaQuery.of(context).size; - final isLoading = context.watch().state is ApiAuthLoading; + final isLoading = context.watch().state is AuthLoading; if (_showShimmer) { return const LoginShimmerPage(); @@ -201,7 +218,6 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( margin: const EdgeInsets.only(bottom: 48), decoration: BoxDecoration( @@ -229,7 +245,6 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { ), ), - Text( 'Welcome Back', style: textTheme.headlineLarge?.copyWith( @@ -250,7 +265,6 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { ), const SizedBox(height: 48), - Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), @@ -264,7 +278,9 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { ), child: TextFormField( controller: _usernameController, - style: textTheme.bodyLarge?.copyWith(color: colors.onSurface), + style: textTheme.bodyLarge?.copyWith( + color: colors.onSurface, + ), decoration: InputDecoration( labelText: 'Username', hintText: 'Inserisci il tuo username', @@ -282,11 +298,17 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.primary, width: 2.0), + borderSide: BorderSide( + color: colors.primary, + width: 2.0, + ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.error, width: 2.0), + borderSide: BorderSide( + color: colors.error, + width: 2.0, + ), ), filled: true, fillColor: colors.surface, @@ -320,7 +342,9 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { ), child: TextFormField( controller: _passwordController, - style: textTheme.bodyLarge?.copyWith(color: colors.onSurface), + style: textTheme.bodyLarge?.copyWith( + color: colors.onSurface, + ), decoration: InputDecoration( labelText: 'Password', hintText: 'Enter your password', @@ -338,11 +362,17 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.primary, width: 2.0), + borderSide: BorderSide( + color: colors.primary, + width: 2.0, + ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.error, width: 2.0), + borderSide: BorderSide( + color: colors.error, + width: 2.0, + ), ), filled: true, fillColor: colors.surface, @@ -376,7 +406,10 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { }, style: TextButton.styleFrom( foregroundColor: colors.primary, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), textStyle: textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), @@ -386,7 +419,6 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { ), const SizedBox(height: 32), - SizedBox( width: double.infinity, child: FilledButton( @@ -405,26 +437,28 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { letterSpacing: 0.5, ), ), - child: isLoading - ? SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - strokeWidth: 2.5, - color: colors.onPrimary, - ), - ) - : const Text('Sign In'), + child: + isLoading + ? SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: colors.onPrimary, + ), + ) + : const Text('Sign In'), ), ), const SizedBox(height: 32), - Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: colors.surfaceContainerHighest.withOpacity(0.3), + color: colors.surfaceContainerHighest.withOpacity( + 0.3, + ), borderRadius: BorderRadius.circular(16), ), child: Row( @@ -444,7 +478,9 @@ class _LoginFormState extends State<_LoginForm> with TickerProviderStateMixin { }, style: TextButton.styleFrom( foregroundColor: colors.primary, - padding: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), textStyle: textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), diff --git a/lib/features/auth/presentation/pages/register_page.dart b/lib/features/auth/presentation/pages/register_page.dart index 315c8c2..0f9d7b1 100644 --- a/lib/features/auth/presentation/pages/register_page.dart +++ b/lib/features/auth/presentation/pages/register_page.dart @@ -1,18 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; -import 'package:ketchapp_flutter/features/auth/bloc/api_auth_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ketchapp_flutter/features/auth/bloc/auth_bloc.dart'; + import 'register_shimmer_page.dart'; class RegisterPage extends StatefulWidget { - const RegisterPage({super.key}); + const RegisterPage({Key? key}) : super(key: key); @override State createState() => _RegisterPageState(); } -class _RegisterPageState extends State with TickerProviderStateMixin { +class _RegisterPageState extends State + with TickerProviderStateMixin { late AnimationController _fadeAnimationController; late AnimationController _scaleAnimationController; @@ -31,8 +33,6 @@ class _RegisterPageState extends State with TickerProviderStateMix duration: const Duration(milliseconds: 600), vsync: this, ); - - } @override @@ -48,29 +48,47 @@ class _RegisterPageState extends State with TickerProviderStateMix final SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle( statusBarColor: Colors.transparent, - statusBarIconBrightness: colors.brightness == Brightness.light - ? Brightness.dark - : Brightness.light, + statusBarIconBrightness: + colors.brightness == Brightness.light + ? Brightness.dark + : Brightness.light, statusBarBrightness: colors.brightness, systemNavigationBarColor: colors.surface, - systemNavigationBarIconBrightness: colors.brightness == Brightness.light - ? Brightness.dark - : Brightness.light, + systemNavigationBarIconBrightness: + colors.brightness == Brightness.light + ? Brightness.dark + : Brightness.light, ); return AnnotatedRegion( value: systemUiOverlayStyle, child: Scaffold( backgroundColor: colors.surface, - body: BlocListener( + body: BlocListener( listener: (context, state) { - if (state is ApiAuthFailure) { + if (state is AuthAuthenticated) { if (!mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.error), + content: Text('Registrazione effettuata con successo!'), + backgroundColor: colors.primary, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ); + context.go('/home'); + } + if (state is AuthError) { + if (!mounted) return; + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(state.message), backgroundColor: colors.error, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( @@ -94,7 +112,8 @@ class _RegisterForm extends StatefulWidget { State<_RegisterForm> createState() => _RegisterFormState(); } -class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMixin { +class _RegisterFormState extends State<_RegisterForm> + with TickerProviderStateMixin { final _formKey = GlobalKey(); late final TextEditingController _usernameController; late final TextEditingController _emailController; @@ -137,21 +156,19 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi vsync: this, ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _fadeAnimationController, - curve: Curves.easeOutCubic, - )); - - _scaleAnimation = Tween( - begin: 0.95, - end: 1.0, - ).animate(CurvedAnimation( - parent: _scaleAnimationController, - curve: Curves.easeOutBack, - )); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _fadeAnimationController, + curve: Curves.easeOutCubic, + ), + ); + + _scaleAnimation = Tween(begin: 0.95, end: 1.0).animate( + CurvedAnimation( + parent: _scaleAnimationController, + curve: Curves.easeOutBack, + ), + ); } @override @@ -169,13 +186,13 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi HapticFeedback.lightImpact(); FocusScope.of(context).unfocus(); if (_formKey.currentState!.validate()) { - context.read().add( - ApiAuthRegisterRequested( - username: _usernameController.text.trim(), - email: _emailController.text.trim(), - password: _passwordController.text, - ), - ); + context.read().add( + AuthRegisterRequested( + username: _usernameController.text, + email: _emailController.text, + password: _passwordController.text, + ), + ); } } @@ -184,7 +201,7 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi final ColorScheme colors = Theme.of(context).colorScheme; final TextTheme textTheme = Theme.of(context).textTheme; final Size size = MediaQuery.of(context).size; - final isLoading = context.watch().state is ApiAuthLoading; + final isLoading = context.watch().state is AuthLoading; if (_showShimmer) { return const RegisterShimmerPage(); @@ -210,7 +227,6 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( margin: const EdgeInsets.only(bottom: 48), decoration: BoxDecoration( @@ -257,7 +273,6 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi ), const SizedBox(height: 48), - Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), @@ -271,7 +286,9 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi ), child: TextFormField( controller: _usernameController, - style: textTheme.bodyLarge?.copyWith(color: colors.onSurface), + style: textTheme.bodyLarge?.copyWith( + color: colors.onSurface, + ), decoration: InputDecoration( labelText: 'Username', hintText: 'Choose a unique username', @@ -289,11 +306,17 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.primary, width: 2.0), + borderSide: BorderSide( + color: colors.primary, + width: 2.0, + ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.error, width: 2.0), + borderSide: BorderSide( + color: colors.error, + width: 2.0, + ), ), filled: true, fillColor: colors.surface, @@ -330,7 +353,9 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi ), child: TextFormField( controller: _emailController, - style: textTheme.bodyLarge?.copyWith(color: colors.onSurface), + style: textTheme.bodyLarge?.copyWith( + color: colors.onSurface, + ), decoration: InputDecoration( labelText: 'Email', hintText: 'Enter your email address', @@ -348,11 +373,17 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.primary, width: 2.0), + borderSide: BorderSide( + color: colors.primary, + width: 2.0, + ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.error, width: 2.0), + borderSide: BorderSide( + color: colors.error, + width: 2.0, + ), ), filled: true, fillColor: colors.surface, @@ -367,7 +398,9 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi if (value == null || value.isEmpty) { return 'Please enter your email'; } - if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { + if (!RegExp( + r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$', + ).hasMatch(value)) { return 'Please enter a valid email address'; } return null; @@ -389,7 +422,9 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi ), child: TextFormField( controller: _passwordController, - style: textTheme.bodyLarge?.copyWith(color: colors.onSurface), + style: textTheme.bodyLarge?.copyWith( + color: colors.onSurface, + ), decoration: InputDecoration( labelText: 'Password', hintText: 'Create a strong password', @@ -407,11 +442,17 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.primary, width: 2.0), + borderSide: BorderSide( + color: colors.primary, + width: 2.0, + ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.error, width: 2.0), + borderSide: BorderSide( + color: colors.error, + width: 2.0, + ), ), filled: true, fillColor: colors.surface, @@ -448,7 +489,9 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi ), child: TextFormField( controller: _confirmPasswordController, - style: textTheme.bodyLarge?.copyWith(color: colors.onSurface), + style: textTheme.bodyLarge?.copyWith( + color: colors.onSurface, + ), decoration: InputDecoration( labelText: 'Confirm Password', hintText: 'Confirm your password', @@ -466,11 +509,17 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.primary, width: 2.0), + borderSide: BorderSide( + color: colors.primary, + width: 2.0, + ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide(color: colors.error, width: 2.0), + borderSide: BorderSide( + color: colors.error, + width: 2.0, + ), ), filled: true, fillColor: colors.surface, @@ -508,32 +557,36 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi borderRadius: BorderRadius.circular(16), ), elevation: 2, - shadowColor: colors.primary.withValues(alpha: 0.3), + shadowColor: colors.primary.withValues( + alpha: 0.3, + ), textStyle: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), - child: isLoading - ? SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - strokeWidth: 2.5, - color: colors.onPrimary, - ), - ) - : const Text('Create Account'), + child: + isLoading + ? SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: colors.onPrimary, + ), + ) + : const Text('Create Account'), ), ), const SizedBox(height: 32), - Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: colors.surfaceContainerHighest.withValues(alpha: 0.3), + color: colors.surfaceContainerHighest.withValues( + alpha: 0.3, + ), borderRadius: BorderRadius.circular(16), ), child: Row( @@ -542,7 +595,9 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi Text( "Already have an account?", style: textTheme.bodyMedium?.copyWith( - color: colors.onSurface.withValues(alpha: 0.7), + color: colors.onSurface.withValues( + alpha: 0.7, + ), ), ), const SizedBox(width: 8), @@ -553,7 +608,9 @@ class _RegisterFormState extends State<_RegisterForm> with TickerProviderStateMi }, style: TextButton.styleFrom( foregroundColor: colors.primary, - padding: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), textStyle: textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), diff --git a/lib/features/home/presentation/widgets/todays_tomatoes_card.dart b/lib/features/home/presentation/widgets/todays_tomatoes_card.dart index 833da67..9e98814 100644 --- a/lib/features/home/presentation/widgets/todays_tomatoes_card.dart +++ b/lib/features/home/presentation/widgets/todays_tomatoes_card.dart @@ -15,7 +15,7 @@ class TodaysTomatoesCard extends StatefulWidget { class _TodaysTomatoesCardState extends State with TickerProviderStateMixin { - late Future> _tomatoesFuture; + late Future> _tomatoesFuture = Future.value([]); late AnimationController _pulseAnimationController; late AnimationController _slideAnimationController; late Animation _pulseAnimation; @@ -28,11 +28,21 @@ class _TodaysTomatoesCardState extends State super.initState(); _initializeAnimations(); _scrollController = ScrollController(); + _loadTomatoes(); + } + + Future _loadTomatoes() async { final authState = context.read().state; - if (authState is Authenticated) { - _tomatoesFuture = ApiService().getTodaysTomatoes(authState.userUuid); + final apiService = context.read(); + if (authState is AuthAuthenticated) { + final tomatoes = await apiService.getTodaysTomatoes(); + setState(() { + _tomatoesFuture = Future.value(tomatoes); + }); } else { - _tomatoesFuture = Future.value([]); + setState(() { + _tomatoesFuture = Future.value([]); + }); } } @@ -46,29 +56,26 @@ class _TodaysTomatoesCardState extends State vsync: this, ); - _pulseAnimation = Tween( - begin: 1.0, - end: 1.02, - ).animate(CurvedAnimation( - parent: _pulseAnimationController, - curve: Curves.easeInOut, - )); + _pulseAnimation = Tween(begin: 1.0, end: 1.02).animate( + CurvedAnimation( + parent: _pulseAnimationController, + curve: Curves.easeInOut, + ), + ); _slideAnimation = Tween( begin: const Offset(0, 0.3), end: Offset.zero, - ).animate(CurvedAnimation( - parent: _slideAnimationController, - curve: Curves.easeOutCubic, - )); + ).animate( + CurvedAnimation( + parent: _slideAnimationController, + curve: Curves.easeOutCubic, + ), + ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _slideAnimationController, - curve: Curves.easeOut, - )); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _slideAnimationController, curve: Curves.easeOut), + ); _slideAnimationController.forward(); _pulseAnimationController.repeat(reverse: true); @@ -85,18 +92,16 @@ class _TodaysTomatoesCardState extends State @override Widget build(BuildContext context) { return BlocListener( - listener: (context, state) { - if (state is Authenticated) { + listener: (context, state) async { + if (state is AuthAuthenticated) { + final apiService = context.read(); + final tomatoes = await apiService.getTodaysTomatoes(); + final uniqueSubjects = {}; + for (final tomato in tomatoes) { + uniqueSubjects[tomato.subject] = tomato; + } setState(() { - _tomatoesFuture = ApiService() - .getTodaysTomatoes(state.userUuid) - .then((tomatoes) { - final uniqueSubjects = {}; - for (final tomato in tomatoes) { - uniqueSubjects[tomato.subject] = tomato; - } - return uniqueSubjects.values.toList(); - }); + _tomatoesFuture = Future.value(uniqueSubjects.values.toList()); }); _slideAnimationController.reset(); _slideAnimationController.forward(); @@ -141,9 +146,7 @@ class _TodaysTomatoesCardState extends State ], ), borderRadius: BorderRadius.circular(24), - border: Border.all( - color: colors.outline.withValues(alpha: 0.1), - ), + border: Border.all(color: colors.outline.withValues(alpha: 0.1)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -218,9 +221,7 @@ class _TodaysTomatoesCardState extends State ], ), borderRadius: BorderRadius.circular(24), - border: Border.all( - color: colors.error.withValues(alpha: 0.1), - ), + border: Border.all(color: colors.error.withValues(alpha: 0.1)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -265,9 +266,7 @@ class _TodaysTomatoesCardState extends State decoration: BoxDecoration( color: colors.surfaceContainerHighest.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(16), - border: Border.all( - color: colors.outline.withValues(alpha: 0.1), - ), + border: Border.all(color: colors.outline.withValues(alpha: 0.1)), ), child: Text( 'Check your connection and try again', @@ -298,9 +297,7 @@ class _TodaysTomatoesCardState extends State ], ), borderRadius: BorderRadius.circular(24), - border: Border.all( - color: colors.outline.withValues(alpha: 0.08), - ), + border: Border.all(color: colors.outline.withValues(alpha: 0.08)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -328,9 +325,7 @@ class _TodaysTomatoesCardState extends State decoration: BoxDecoration( color: colors.surfaceContainerHighest.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(20), - border: Border.all( - color: colors.outline.withValues(alpha: 0.1), - ), + border: Border.all(color: colors.outline.withValues(alpha: 0.1)), ), child: Text( 'Create your first focus session to boost productivity', @@ -403,7 +398,12 @@ class _TodaysTomatoesCardState extends State ); } - Widget _buildTomatoCard(BuildContext context, Tomato tomato, ColorScheme colors, TextTheme textTheme) { + Widget _buildTomatoCard( + BuildContext context, + Tomato tomato, + ColorScheme colors, + TextTheme textTheme, + ) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), @@ -485,9 +485,14 @@ class _TodaysTomatoesCardState extends State ), const SizedBox(height: 8), Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( - color: colors.surfaceContainerHighest.withValues(alpha: 0.8), + color: colors.surfaceContainerHighest.withValues( + alpha: 0.8, + ), borderRadius: BorderRadius.circular(12), border: Border.all( color: colors.outline.withValues(alpha: 0.1), @@ -503,7 +508,9 @@ class _TodaysTomatoesCardState extends State ), const SizedBox(width: 6), Text( - TimeOfDay.fromDateTime(tomato.startAt.toLocal()).format(context), + TimeOfDay.fromDateTime( + tomato.startAt.toLocal(), + ).format(context), style: textTheme.bodyMedium?.copyWith( color: colors.onSurfaceVariant, fontWeight: FontWeight.w600, diff --git a/lib/features/plan/presentation/pages/plan_creation_loading_page.dart b/lib/features/plan/presentation/pages/plan_creation_loading_page.dart index b88945d..cb21fdc 100644 --- a/lib/features/plan/presentation/pages/plan_creation_loading_page.dart +++ b/lib/features/plan/presentation/pages/plan_creation_loading_page.dart @@ -31,7 +31,7 @@ class _PlanCreationLoadingPageState extends State { try { await ApiService().createPlan(widget.plan); final api = ApiService(); - final tomatoes = await api.getTodaysTomatoes(widget.plan.userUUID); + final tomatoes = await api.getTodaysTomatoes(); if (mounted && tomatoes.isNotEmpty) { context.go('/timer/${tomatoes.first.id}'); } else if (mounted) { diff --git a/lib/features/profile/bloc/achievement_bloc.dart b/lib/features/profile/bloc/achievement_bloc.dart index ea3de2d..d122117 100644 --- a/lib/features/profile/bloc/achievement_bloc.dart +++ b/lib/features/profile/bloc/achievement_bloc.dart @@ -10,20 +10,21 @@ class AchievementBloc extends Bloc { final AuthBloc _authBloc; AchievementBloc({required ApiService apiService, required AuthBloc authBloc}) - : _apiService = apiService, - _authBloc = authBloc, - super(AchievementInitial()) { + : _apiService = apiService, + _authBloc = authBloc, + super(AchievementInitial()) { on(_onLoadAchievements); } Future _onLoadAchievements( - LoadAchievements event, Emitter emit) async { + LoadAchievements event, + Emitter emit, + ) async { final authState = _authBloc.state; - if (authState is Authenticated) { + if (authState is AuthAuthenticated) { emit(AchievementLoading()); try { - final achievementsData = await _apiService.getAchievements(authState.userUuid); - final achievements = achievementsData.map((json) => Achievement.fromJson(json)).toList(); + final achievements = await _apiService.getUserAchievements(); emit(AchievementLoaded(achievements)); } catch (e) { emit(AchievementError(e.toString())); diff --git a/lib/features/profile/bloc/api_profile_bloc.dart b/lib/features/profile/bloc/api_profile_bloc.dart index 72e5951..d31b8c1 100644 --- a/lib/features/profile/bloc/api_profile_bloc.dart +++ b/lib/features/profile/bloc/api_profile_bloc.dart @@ -2,31 +2,21 @@ import 'dart:async'; import 'dart:io'; import 'package:bloc/bloc.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:ketchapp_flutter/features/auth/bloc/api_auth_bloc.dart'; + import 'package:ketchapp_flutter/services/api_service.dart'; import 'api_profile_event.dart'; import 'api_profile_state.dart'; class ApiProfileBloc extends Bloc { final ApiService _apiService; - final ApiAuthBloc _apiAuthBloc; final ImagePicker _imagePicker; - StreamSubscription? _authSubscription; ApiProfileBloc({ required ApiService apiService, - required ApiAuthBloc apiAuthBloc, required ImagePicker imagePicker, - }) : _apiService = apiService, - _apiAuthBloc = apiAuthBloc, - _imagePicker = imagePicker, - super(ApiProfileInitial()) { - _authSubscription = _apiAuthBloc.stream.listen((state) { - if (state is ApiAuthenticated) { - add(LoadApiProfile()); - } - }); - + }) : _apiService = apiService, + _imagePicker = imagePicker, + super(ApiProfileInitial()) { on(_onLoadProfile); on(_onProfileImagePickRequested); on(_onProfileImageUploadRequested); @@ -34,32 +24,36 @@ class ApiProfileBloc extends Bloc { } Future _onLoadProfile( - LoadApiProfile event, Emitter emit) async { - final authState = _apiAuthBloc.state; - if (authState is ApiAuthenticated) { - try { - emit(ApiProfileLoading()); - final userUuid = authState.userData['uuid']; - final userData = await _apiService.fetchData('users/$userUuid'); - final allAchievements = await _apiService.getAllAchievements(); - final completedAchievements = await _apiService.getUserAchievements(userUuid); + LoadApiProfile event, + Emitter emit, + ) async { + try { + emit(ApiProfileLoading()); + final userData = await _apiService.getCurrentUser(); + final userAchievements = await _apiService.getUserAchievements(); - emit(ApiProfileLoaded( - userData: userData, - allAchievements: allAchievements, - completedAchievementTitles: completedAchievements.map((a) => a.title).toSet(), - )); - } catch (e) { - emit(ApiProfileError('Errore nel caricamento del profilo: ${e.toString()}')); - } + emit( + ApiProfileLoaded(userData: userData, allAchievements: userAchievements), + ); + } catch (e) { + emit( + ApiProfileError('Errore nel caricamento del profilo: ${e.toString()}'), + ); } } Future _onProfileImagePickRequested( - ApiProfileImagePickRequested event, Emitter emit) async { + ApiProfileImagePickRequested event, + Emitter emit, + ) async { if (state is ApiProfileLoaded) { final currentState = state as ApiProfileLoaded; - emit(currentState.copyWith(isUploadingImage: true, clearLocalPreviewFile: true)); + emit( + currentState.copyWith( + isUploadingImage: true, + clearLocalPreviewFile: true, + ), + ); try { final XFile? pickedFile = await _imagePicker.pickImage( source: event.source, @@ -69,27 +63,49 @@ class ApiProfileBloc extends Bloc { if (pickedFile != null) { add(ApiProfileImageUploadRequested(File(pickedFile.path))); } else { - emit(currentState.copyWith(isUploadingImage: false, clearLocalPreviewFile: true)); + emit( + currentState.copyWith( + isUploadingImage: false, + clearLocalPreviewFile: true, + ), + ); } } catch (e) { - emit(ApiProfileError('Errore durante la selezione dell\'immagine: ${e.toString()}')); - emit(currentState.copyWith(isUploadingImage: false, clearLocalPreviewFile: true)); + emit( + ApiProfileError( + 'Errore durante la selezione dell\'immagine: ${e.toString()}', + ), + ); + emit( + currentState.copyWith( + isUploadingImage: false, + clearLocalPreviewFile: true, + ), + ); } } } Future _onProfileImageUploadRequested( - ApiProfileImageUploadRequested event, Emitter emit) async { + ApiProfileImageUploadRequested event, + Emitter emit, + ) async { if (state is ApiProfileLoaded) { final currentState = state as ApiProfileLoaded; try { - final userUuid = currentState.userData['uuid']; - final newUserData = await _apiService.uploadProfilePicture(userUuid, event.imageFile); - emit(currentState.copyWith( - userData: newUserData, - isUploadingImage: false, - clearLocalPreviewFile: true, - )); + final userUuid = currentState.userData['id']; + // Chiamata API custom per upload immagine profilo + final response = await _apiService.postData( + 'users/$userUuid/profile-picture', + {'filePath': event.imageFile.path}, + ); + emit( + currentState.copyWith( + userData: response, + isUploadingImage: false, + clearLocalPreviewFile: true, + ), + ); emit(const ApiProfileUpdateSuccess('Immagine del profilo aggiornata.')); } catch (e) { emit(ApiProfileError('Errore durante il caricamento: ${e.toString()}')); @@ -99,20 +115,26 @@ class ApiProfileBloc extends Bloc { } Future _onProfileImageDeleteRequested( - ApiProfileImageDeleteRequested event, Emitter emit) async { + ApiProfileImageDeleteRequested event, + Emitter emit, + ) async { if (state is ApiProfileLoaded) { final currentState = state as ApiProfileLoaded; emit(currentState.copyWith(isUploadingImage: true)); try { - final userUuid = currentState.userData['uuid']; - final newUserData = await _apiService.deleteProfilePicture(userUuid); - emit(currentState.copyWith( - userData: newUserData, - isUploadingImage: false, - )); + final userUuid = currentState.userData['id']; + // Chiamata API custom per eliminare immagine profilo + final response = await _apiService.deleteData( + 'users/$userUuid/profile-picture', + ); + emit( + currentState.copyWith(userData: response, isUploadingImage: false), + ); emit(const ApiProfileUpdateSuccess('Immagine del profilo eliminata.')); } catch (e) { - emit(ApiProfileError('Errore durante l\'eliminazione: ${e.toString()}')); + emit( + ApiProfileError('Errore durante l\'eliminazione: ${e.toString()}'), + ); emit(currentState.copyWith(isUploadingImage: false)); } } @@ -120,7 +142,6 @@ class ApiProfileBloc extends Bloc { @override Future close() { - _authSubscription?.cancel(); return super.close(); } } diff --git a/lib/features/profile/bloc/profile_bloc.dart b/lib/features/profile/bloc/profile_bloc.dart index 75ba7a6..4b91066 100644 --- a/lib/features/profile/bloc/profile_bloc.dart +++ b/lib/features/profile/bloc/profile_bloc.dart @@ -1,7 +1,5 @@ import 'dart:io'; import 'package:bloc/bloc.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:image_picker/image_picker.dart'; import 'profile_event.dart'; import 'profile_state.dart'; @@ -9,24 +7,18 @@ import 'package:ketchapp_flutter/services/api_service.dart'; import 'package:ketchapp_flutter/features/auth/bloc/auth_bloc.dart'; class ProfileBloc extends Bloc { - final FirebaseAuth _firebaseAuth; - final FirebaseStorage _firebaseStorage; final ImagePicker _imagePicker; final ApiService _apiService; final AuthBloc _authBloc; ProfileBloc({ - required FirebaseAuth firebaseAuth, - required FirebaseStorage firebaseStorage, required ImagePicker imagePicker, required ApiService apiService, required AuthBloc authBloc, - }) : _apiService = apiService, - _authBloc = authBloc, - _firebaseAuth = firebaseAuth, - _firebaseStorage = firebaseStorage, - _imagePicker = imagePicker, - super(ProfileInitial()) { + }) : _apiService = apiService, + _authBloc = authBloc, + _imagePicker = imagePicker, + super(ProfileInitial()) { on(_onLoadProfile); on(_onProfileImagePickRequested); on(_onProfileImageUploadRequested); @@ -35,53 +27,44 @@ class ProfileBloc extends Bloc { } Future _onLoadProfile( - LoadProfile event, Emitter emit) async { - final user = _firebaseAuth.currentUser; - String? username; - if (user != null) { - String? userUuid; - final authState = _authBloc.state; - if (authState is Authenticated) { - userUuid = authState.userUuid; - } - if (userUuid != null) { - try { - final userData = await _apiService.fetchData('users/$userUuid'); - username = userData['username'] as String?; - } catch (e) { - } - } - if (state is ProfileLoaded) { - emit((state as ProfileLoaded).copyWith( - username: username, - displayName: user.displayName, - email: user.email, - photoUrl: user.photoURL, - isUploadingImage: false, - clearLocalPreviewFile: true, - )); - } else { - emit(ProfileLoaded( - username: username, - displayName: user.displayName, - email: user.email, - photoUrl: user.photoURL, - isUploadingImage: false, - localPreviewFile: null, - )); + LoadProfile event, + Emitter emit, + ) async { + final authState = _authBloc.state; + if (authState is AuthAuthenticated) { + try { + final userData = await _apiService.fetchData('users/${authState.id}'); + emit( + ProfileLoaded( + username: userData['username'] as String?, + displayName: userData['username'] as String?, + email: userData['email'] as String?, + photoUrl: userData['photoUrl'] as String?, + isUploadingImage: false, + localPreviewFile: null, + ), + ); + add(LoadAchievements()); + } catch (e) { + emit(ProfileError('Errore nel caricamento profilo: ${e.toString()}')); } - - add(LoadAchievements()); } else { - emit(const ProfileError('Utente non trovato.')); + emit(const ProfileError('Utente non autenticato.')); } } Future _onProfileImagePickRequested( - ProfileImagePickRequested event, Emitter emit) async { + ProfileImagePickRequested event, + Emitter emit, + ) async { if (state is ProfileLoaded) { final currentState = state as ProfileLoaded; - emit(currentState.copyWith(isUploadingImage: true, clearLocalPreviewFile: true)); + emit( + currentState.copyWith( + isUploadingImage: true, + clearLocalPreviewFile: true, + ), + ); try { final XFile? pickedFile = await _imagePicker.pickImage( source: event.source, @@ -89,146 +72,174 @@ class ProfileBloc extends Bloc { maxWidth: 800, ); if (pickedFile != null) { - emit(currentState.copyWith( - localPreviewFile: File(pickedFile.path), - isUploadingImage: true, - )); + emit( + currentState.copyWith( + localPreviewFile: File(pickedFile.path), + isUploadingImage: true, + ), + ); add(ProfileImageUploadRequested(File(pickedFile.path))); } else { - emit(currentState.copyWith(isUploadingImage: false, clearLocalPreviewFile: true)); + emit( + currentState.copyWith( + isUploadingImage: false, + clearLocalPreviewFile: true, + ), + ); } } catch (e) { - emit(ProfileError('Errore durante la selezione dell\'immagine: ${e.toString()}')); - emit(currentState.copyWith(isUploadingImage: false, clearLocalPreviewFile: true)); + emit( + ProfileError( + 'Errore durante la selezione dell\'immagine: ${e.toString()}', + ), + ); + emit( + currentState.copyWith( + isUploadingImage: false, + clearLocalPreviewFile: true, + ), + ); } } else { - emit(const ProfileError('Profilo non caricato. Impossibile selezionare l\'immagine.')); + emit( + const ProfileError( + 'Profilo non caricato. Impossibile selezionare l\'immagine.', + ), + ); } } Future _onProfileImageUploadRequested( - ProfileImageUploadRequested event, Emitter emit) async { - final user = _firebaseAuth.currentUser; - if (user == null) { - emit(const ProfileError('Utente non autenticato per caricare l\'immagine.')); + ProfileImageUploadRequested event, + Emitter emit, + ) async { + final authState = _authBloc.state; + if (authState is! AuthAuthenticated) { + emit( + const ProfileError('Utente non autenticato per caricare l\'immagine.'), + ); if (state is ProfileLoaded) { - emit((state as ProfileLoaded).copyWith(isUploadingImage: false, clearLocalPreviewFile: true)); + emit( + (state as ProfileLoaded).copyWith( + isUploadingImage: false, + clearLocalPreviewFile: true, + ), + ); } return; } if (state is ProfileLoaded) { final currentState = state as ProfileLoaded; try { - final filePath = 'profile_pictures/${user.uid}/profile.jpg'; - final ref = _firebaseStorage.ref().child(filePath); - await ref.putFile(event.imageFile); - final downloadUrl = await ref.getDownloadURL(); - await user.updatePhotoURL(downloadUrl); - emit(currentState.copyWith( - photoUrl: downloadUrl, - isUploadingImage: false, - clearLocalPreviewFile: true, - )); + // Chiamata API custom per upload immagine profilo + final response = await _apiService.postData( + 'users/${authState.id}/profile-picture', + {'filePath': event.imageFile.path}, + ); + emit( + currentState.copyWith( + photoUrl: response['photoUrl'], + isUploadingImage: false, + clearLocalPreviewFile: true, + ), + ); emit(const ProfileUpdateSuccess('Immagine del profilo aggiornata.')); add(LoadProfile()); - } on FirebaseException catch (e) { - emit(ProfileError('Errore Firebase durante il caricamento: ${e.message}')); - emit(currentState.copyWith(isUploadingImage: false)); } catch (e) { - emit(ProfileError('Errore sconosciuto durante il caricamento: ${e.toString()}')); + emit(ProfileError('Errore durante il caricamento: ${e.toString()}')); emit(currentState.copyWith(isUploadingImage: false)); } } } Future _onProfileImageDeleteRequested( - ProfileImageDeleteRequested event, Emitter emit) async { - final user = _firebaseAuth.currentUser; - if (user == null) { - emit(const ProfileError('Utente non autenticato per eliminare l\'immagine.')); + ProfileImageDeleteRequested event, + Emitter emit, + ) async { + final authState = _authBloc.state; + if (authState is! AuthAuthenticated) { + emit( + const ProfileError('Utente non autenticato per eliminare l\'immagine.'), + ); return; } if (state is ProfileLoaded) { final currentState = state as ProfileLoaded; - emit(currentState.copyWith(isUploadingImage: true, clearLocalPreviewFile: true)); - try { - if (user.photoURL != null) { - try { - final String filePath = 'profile_pictures/${user.uid}/profile.jpg'; - final ref = _firebaseStorage.ref().child(filePath); - await ref.delete(); - } on FirebaseException { - } - } - await user.updatePhotoURL(null); - emit(currentState.copyWith( - photoUrl: null, - isUploadingImage: false, + emit( + currentState.copyWith( + isUploadingImage: true, clearLocalPreviewFile: true, - )); + ), + ); + try { + // Chiamata API custom per eliminare immagine profilo + await _apiService.deleteData('users/${authState.id}/profile-picture'); + emit( + currentState.copyWith( + photoUrl: null, + isUploadingImage: false, + clearLocalPreviewFile: true, + ), + ); emit(const ProfileUpdateSuccess('Immagine del profilo eliminata.')); add(LoadProfile()); - } on FirebaseException catch (e) { - emit(ProfileError('Errore Firebase durante l\'eliminazione: ${e.message}')); - emit(currentState.copyWith(isUploadingImage: false)); } catch (e) { - emit(ProfileError('Errore sconosciuto durante l\'eliminazione: ${e.toString()}')); + emit(ProfileError('Errore durante l\'eliminazione: ${e.toString()}')); emit(currentState.copyWith(isUploadingImage: false)); } } else { - emit(const ProfileError('Profilo non caricato. Impossibile eliminare l\'immagine.')); + emit( + const ProfileError( + 'Profilo non caricato. Impossibile eliminare l\'immagine.', + ), + ); } } Future _onLoadAchievements( - LoadAchievements event, Emitter emit) async { - final user = _firebaseAuth.currentUser; - if (user == null) { + LoadAchievements event, + Emitter emit, + ) async { + final authState = _authBloc.state; + if (authState is! AuthAuthenticated) { if (state is ProfileLoaded) { - emit((state as ProfileLoaded).copyWith( - achievementsLoading: false, - achievementsError: 'Utente non autenticato.', - )); + emit( + (state as ProfileLoaded).copyWith( + achievementsLoading: false, + achievementsError: 'Utente non autenticato.', + ), + ); } return; } if (state is ProfileLoaded) { - emit((state as ProfileLoaded).copyWith( - achievementsLoading: true, - achievementsError: null, - )); + emit( + (state as ProfileLoaded).copyWith( + achievementsLoading: true, + achievementsError: null, + ), + ); } try { - String? userUuid; - final authState = _authBloc.state; - if (authState is Authenticated) { - userUuid = authState.userUuid; - } - if (userUuid != null) { - final all = await _apiService.getAllAchievements(); - final completed = await _apiService.getUserAchievements(userUuid); - if (state is ProfileLoaded) { - emit((state as ProfileLoaded).copyWith( - allAchievements: all, + final completed = await _apiService.getUserAchievements(); + if (state is ProfileLoaded) { + emit( + (state as ProfileLoaded).copyWith( completedAchievementTitles: completed.map((a) => a.title).toSet(), achievementsLoading: false, achievementsError: null, - )); - } - } else { - if (state is ProfileLoaded) { - emit((state as ProfileLoaded).copyWith( - achievementsLoading: false, - achievementsError: 'Utente non autenticato.', - )); - } + ), + ); } } catch (e) { if (state is ProfileLoaded) { - emit((state as ProfileLoaded).copyWith( - achievementsLoading: false, - achievementsError: 'Errore nel caricamento achievements: ${e.toString()}', - )); + emit( + (state as ProfileLoaded).copyWith( + achievementsLoading: false, + achievementsError: + 'Errore nel caricamento achievements: ${e.toString()}', + ), + ); } } } diff --git a/lib/features/profile/presentation/pages/profile_page.dart b/lib/features/profile/presentation/pages/profile_page.dart index 660b075..3ba03cd 100644 --- a/lib/features/profile/presentation/pages/profile_page.dart +++ b/lib/features/profile/presentation/pages/profile_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:ketchapp_flutter/features/auth/bloc/api_auth_bloc.dart'; +import 'package:ketchapp_flutter/features/auth/bloc/auth_bloc.dart'; import 'package:ketchapp_flutter/features/profile/bloc/api_profile_bloc.dart'; import 'package:ketchapp_flutter/features/profile/bloc/api_profile_event.dart'; import 'package:ketchapp_flutter/features/profile/bloc/api_profile_state.dart'; @@ -55,21 +55,19 @@ class _ProfilePageState extends State vsync: this, ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _fadeAnimationController, - curve: Curves.easeOutCubic, - )); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _fadeAnimationController, + curve: Curves.easeOutCubic, + ), + ); - _scaleAnimation = Tween( - begin: 0.95, - end: 1.0, - ).animate(CurvedAnimation( - parent: _scaleAnimationController, - curve: Curves.easeOutBack, - )); + _scaleAnimation = Tween(begin: 0.95, end: 1.0).animate( + CurvedAnimation( + parent: _scaleAnimationController, + curve: Curves.easeOutBack, + ), + ); } @override @@ -82,7 +80,8 @@ class _ProfilePageState extends State void _dispatchPickImage(ImageSource source) async { final currentBlocState = context.read().state; - if (currentBlocState is ApiProfileLoaded && currentBlocState.isUploadingImage) { + if (currentBlocState is ApiProfileLoaded && + currentBlocState.isUploadingImage) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( @@ -102,7 +101,9 @@ class _ProfilePageState extends State ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: const Text('Permesso fotocamera negato in modo permanente. Abilitalo dalle impostazioni.'), + content: const Text( + 'Permesso fotocamera negato in modo permanente. Abilitalo dalle impostazioni.', + ), action: SnackBarAction( label: 'Impostazioni', onPressed: () => openAppSettings(), @@ -132,7 +133,9 @@ class _ProfilePageState extends State ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: const Text('Permesso libreria foto negato in modo permanente. Abilitalo dalle impostazioni.'), + content: const Text( + 'Permesso libreria foto negato in modo permanente. Abilitalo dalle impostazioni.', + ), action: SnackBarAction( label: 'Impostazioni', onPressed: () => openAppSettings(), @@ -155,7 +158,8 @@ class _ProfilePageState extends State void _dispatchDeleteProfileImage() { final currentBlocState = context.read().state; - if (currentBlocState is ApiProfileLoaded && currentBlocState.isUploadingImage) { + if (currentBlocState is ApiProfileLoaded && + currentBlocState.isUploadingImage) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( @@ -173,14 +177,16 @@ class _ProfilePageState extends State final SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle( statusBarColor: Colors.transparent, - statusBarIconBrightness: colors.brightness == Brightness.light - ? Brightness.dark - : Brightness.light, + statusBarIconBrightness: + colors.brightness == Brightness.light + ? Brightness.dark + : Brightness.light, statusBarBrightness: colors.brightness, systemNavigationBarColor: colors.surface, - systemNavigationBarIconBrightness: colors.brightness == Brightness.light - ? Brightness.dark - : Brightness.light, + systemNavigationBarIconBrightness: + colors.brightness == Brightness.light + ? Brightness.dark + : Brightness.light, ); return AnnotatedRegion( @@ -202,12 +208,19 @@ class _ProfilePageState extends State listener: _handleProfileStateChanges, child: BlocBuilder( builder: (context, state) { - if (_showShimmer || state is ApiProfileLoading || state is ApiProfileInitial) { + if (_showShimmer || + state is ApiProfileLoading || + state is ApiProfileInitial) { return const ProfileShrimmerPage(); } if (state is ApiProfileError) { - return _buildErrorState(context, state.message, colors, textTheme); + return _buildErrorState( + context, + state.message, + colors, + textTheme, + ); } if (state is ApiProfileLoaded) { @@ -229,25 +242,37 @@ class _ProfilePageState extends State if (state is ApiProfileUpdateSuccess) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() - ..showSnackBar(SnackBar( - content: Text(state.message), - backgroundColor: colors.primary, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - )); + ..showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: colors.primary, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); } else if (state is ApiProfileError) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() - ..showSnackBar(SnackBar( - content: Text(state.message), - backgroundColor: colors.error, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - )); + ..showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: colors.error, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); } } - Widget _buildInitializingState(BuildContext context, ColorScheme colors, TextTheme textTheme) { + Widget _buildInitializingState( + BuildContext context, + ColorScheme colors, + TextTheme textTheme, + ) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -283,7 +308,12 @@ class _ProfilePageState extends State ); } - Widget _buildErrorState(BuildContext context, String error, ColorScheme colors, TextTheme textTheme) { + Widget _buildErrorState( + BuildContext context, + String error, + ColorScheme colors, + TextTheme textTheme, + ) { return Center( child: Padding( padding: const EdgeInsets.all(32), @@ -337,11 +367,15 @@ class _ProfilePageState extends State FilledButton.tonalIcon( icon: const Icon(Icons.refresh_rounded), label: const Text('Try Again'), - onPressed: () => context.read().add(LoadApiProfile()), + onPressed: + () => context.read().add(LoadApiProfile()), style: FilledButton.styleFrom( backgroundColor: colors.primaryContainer, foregroundColor: colors.onPrimaryContainer, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), @@ -353,7 +387,12 @@ class _ProfilePageState extends State ); } - Widget _buildLoadedState(BuildContext context, ApiProfileLoaded state, ColorScheme colors, TextTheme textTheme) { + Widget _buildLoadedState( + BuildContext context, + ApiProfileLoaded state, + ColorScheme colors, + TextTheme textTheme, + ) { return SafeArea( child: FadeTransition( opacity: _fadeAnimation, @@ -370,7 +409,12 @@ class _ProfilePageState extends State const SizedBox(height: 24), _buildProfileInfoSection(context, state, colors, textTheme), const SizedBox(height: 24), - _buildAchievementsSection(context, state, colors, textTheme), + _buildAchievementsSection( + context, + state, + colors, + textTheme, + ), const SizedBox(height: 24), _buildLogoutSection(context, colors, textTheme), const SizedBox(height: 32), @@ -384,7 +428,12 @@ class _ProfilePageState extends State ); } - Widget _buildProfileHeader(BuildContext context, ApiProfileLoaded state, ColorScheme colors, TextTheme textTheme) { + Widget _buildProfileHeader( + BuildContext context, + ApiProfileLoaded state, + ColorScheme colors, + TextTheme textTheme, + ) { return Column( children: [ Container( @@ -480,10 +529,7 @@ class _ProfilePageState extends State decoration: BoxDecoration( color: colors.primaryContainer, shape: BoxShape.circle, - border: Border.all( - color: colors.surface, - width: 2, - ), + border: Border.all(color: colors.surface, width: 2), ), child: PopupMenuButton( icon: Icon( @@ -504,36 +550,46 @@ class _ProfilePageState extends State _dispatchDeleteProfileImage(); } }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: 'take_photo', - child: ListTile( - leading: Icon(Icons.photo_camera_rounded, color: colors.primary), - title: const Text('Take Photo'), - contentPadding: EdgeInsets.zero, - ), - ), - PopupMenuItem( - value: 'library_photo', - child: ListTile( - leading: Icon(Icons.photo_library_rounded, color: colors.primary), - title: const Text('Photo Library'), - contentPadding: EdgeInsets.zero, - ), - ), - if (photoUrl != null || state.localPreviewFile != null) - PopupMenuItem( - value: 'delete_photo', - child: ListTile( - leading: Icon(Icons.delete_rounded, color: colors.error), - title: Text( - 'Delete Photo', - style: TextStyle(color: colors.error), + itemBuilder: + (BuildContext context) => >[ + PopupMenuItem( + value: 'take_photo', + child: ListTile( + leading: Icon( + Icons.photo_camera_rounded, + color: colors.primary, + ), + title: const Text('Take Photo'), + contentPadding: EdgeInsets.zero, ), - contentPadding: EdgeInsets.zero, ), - ), - ], + PopupMenuItem( + value: 'library_photo', + child: ListTile( + leading: Icon( + Icons.photo_library_rounded, + color: colors.primary, + ), + title: const Text('Photo Library'), + contentPadding: EdgeInsets.zero, + ), + ), + if (photoUrl != null || state.localPreviewFile != null) + PopupMenuItem( + value: 'delete_photo', + child: ListTile( + leading: Icon( + Icons.delete_rounded, + color: colors.error, + ), + title: Text( + 'Delete Photo', + style: TextStyle(color: colors.error), + ), + contentPadding: EdgeInsets.zero, + ), + ), + ], ), ), ), @@ -541,7 +597,12 @@ class _ProfilePageState extends State ); } - Widget _buildProfileInfoSection(BuildContext context, ApiProfileLoaded state, ColorScheme colors, TextTheme textTheme) { + Widget _buildProfileInfoSection( + BuildContext context, + ApiProfileLoaded state, + ColorScheme colors, + TextTheme textTheme, + ) { return Container( decoration: BoxDecoration( color: colors.surfaceContainerLow, @@ -604,7 +665,9 @@ class _ProfilePageState extends State Text( "Your account details", style: textTheme.bodySmall?.copyWith( - color: colors.onPrimaryContainer.withAlpha((255 * 0.8).round()), + color: colors.onPrimaryContainer.withAlpha( + (255 * 0.8).round(), + ), ), ), ], @@ -627,7 +690,10 @@ class _ProfilePageState extends State const SizedBox(height: 12), _buildInfoField( 'Email', - state.userData['email'] ?? 'N/A', + state.userData['email'] != null && + state.userData['email'].toString().isNotEmpty + ? state.userData['email'] + : 'N/A', Icons.email_outlined, colors, textTheme, @@ -641,7 +707,13 @@ class _ProfilePageState extends State ); } - Widget _buildInfoField(String label, String value, IconData icon, ColorScheme colors, TextTheme textTheme) { + Widget _buildInfoField( + String label, + String value, + IconData icon, + ColorScheme colors, + TextTheme textTheme, + ) { return Container( decoration: BoxDecoration( color: colors.surfaceContainerHigh.withAlpha((255 * 0.6).round()), @@ -658,11 +730,7 @@ class _ProfilePageState extends State color: colors.primaryContainer.withAlpha((255 * 0.8).round()), borderRadius: BorderRadius.circular(12), ), - child: Icon( - icon, - color: colors.primary, - size: 20, - ), + child: Icon(icon, color: colors.primary, size: 20), ), title: Text( label, @@ -678,16 +746,22 @@ class _ProfilePageState extends State fontWeight: FontWeight.w600, ), ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), ); } - Widget _buildAchievementsSection(BuildContext context, ApiProfileLoaded state, ColorScheme colors, TextTheme textTheme) { - final completedCount = state.completedAchievementTitles?.length ?? 0; - final totalCount = state.allAchievements?.length ?? 0; + Widget _buildAchievementsSection( + BuildContext context, + ApiProfileLoaded state, + ColorScheme colors, + TextTheme textTheme, + ) { + // Usa solo gli achievements dell'utente + final achievements = state.allAchievements ?? []; + final completedCount = + achievements.where((a) => a.completed == true).length; + final totalCount = achievements.length; return Container( decoration: BoxDecoration( @@ -775,14 +849,19 @@ class _ProfilePageState extends State ), const SizedBox(width: 10), Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ colors.primaryContainer, - colors.secondaryContainer.withAlpha((255 * 0.8).round()), + colors.secondaryContainer.withAlpha( + (255 * 0.8).round(), + ), ], ), borderRadius: BorderRadius.circular(12), @@ -810,7 +889,9 @@ class _ProfilePageState extends State Text( '/$totalCount', style: textTheme.titleMedium?.copyWith( - color: colors.onPrimaryContainer.withAlpha((255 * 0.8).round()), + color: colors.onPrimaryContainer.withAlpha( + (255 * 0.8).round(), + ), fontWeight: FontWeight.w600, ), ), @@ -824,11 +905,7 @@ class _ProfilePageState extends State height: 5 * 80.0, child: Stack( children: [ - _buildAchievementsGrid( - state, - colors, - textTheme, - ), + _buildAchievementsGrid(state, colors, textTheme), Positioned.fill( child: IgnorePointer( @@ -855,7 +932,11 @@ class _ProfilePageState extends State ); } - Widget _buildAchievementsGrid(ApiProfileLoaded state, ColorScheme colors, TextTheme textTheme) { + Widget _buildAchievementsGrid( + ApiProfileLoaded state, + ColorScheme colors, + TextTheme textTheme, + ) { if (state.achievementsLoading) { return Container( padding: const EdgeInsets.all(32), @@ -923,7 +1004,8 @@ class _ProfilePageState extends State ), ); } - if (state.allAchievements == null) { + final achievements = state.allAchievements ?? []; + if (achievements.isEmpty) { return const Center(child: Text("No achievements available.")); } return Padding( @@ -943,136 +1025,147 @@ class _ProfilePageState extends State mainAxisSpacing: 16, childAspectRatio: 1.2, ), - itemCount: state.allAchievements!.length, + itemCount: achievements.length, itemBuilder: (context, index) { - final achievement = state.allAchievements![index]; - final isCompleted = state.completedAchievementTitles?.contains(achievement['title']) ?? false; - return _buildAchievementCard(achievement, isCompleted, colors, textTheme); + final achievement = achievements[index]; + final isCompleted = achievement.completed == true; + return _buildAchievementCard( + achievement, + isCompleted, + colors, + textTheme, + ); }, ), ), ); + // Fallback return per evitare errori di return mancante + return const SizedBox.shrink(); } +} - Widget _buildAchievementCard(dynamic achievement, bool isCompleted, ColorScheme colors, TextTheme textTheme) { - final description = achievement['description'] ?? 'No description'; +Widget _buildAchievementCard( + dynamic achievement, + bool isCompleted, + ColorScheme colors, + TextTheme textTheme, +) { + final description = achievement.description ?? 'No description'; - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isCompleted - ? [ + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: + isCompleted + ? [ colors.primaryContainer.withAlpha((255 * 0.9).round()), colors.secondaryContainer.withAlpha((255 * 0.7).round()), ] - : [ + : [ colors.surfaceContainerHigh.withAlpha((255 * 0.8).round()), colors.surfaceContainer.withAlpha((255 * 0.6).round()), ], - ), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: isCompleted - ? colors.primary.withAlpha((255 * 0.4).round()) - : colors.outline.withAlpha((255 * 0.15).round()), - width: isCompleted ? 2 : 1, - ), - boxShadow: [ - BoxShadow( - color: isCompleted - ? colors.primary.withAlpha((255 * 0.15).round()) - : colors.shadow.withAlpha((255 * 0.05).round()), - blurRadius: isCompleted ? 8 : 4, - offset: const Offset(0, 3), - ), - ], ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - description, - style: textTheme.bodyMedium?.copyWith( - color: isCompleted ? colors.onPrimaryContainer : colors.onSurfaceVariant, - fontWeight: FontWeight.w700, - fontSize: 14, - height: 1.2, - ), - textAlign: TextAlign.left, - maxLines: 3, - overflow: TextOverflow.ellipsis, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: + isCompleted + ? colors.primary.withAlpha((255 * 0.4).round()) + : colors.outline.withAlpha((255 * 0.15).round()), + width: isCompleted ? 2 : 1, + ), + boxShadow: [ + BoxShadow( + color: + isCompleted + ? colors.primary.withAlpha((255 * 0.15).round()) + : colors.shadow.withAlpha((255 * 0.05).round()), + blurRadius: isCompleted ? 8 : 4, + offset: const Offset(0, 3), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + description, + style: textTheme.bodyMedium?.copyWith( + color: + isCompleted + ? colors.onPrimaryContainer + : colors.onSurfaceVariant, + fontWeight: FontWeight.w700, + fontSize: 14, + height: 1.2, ), - ], - ), + textAlign: TextAlign.left, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], ), - if (isCompleted) - Padding( - padding: const EdgeInsets.only(left: 8), - child: Container( - width: 28, - height: 28, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - colors.primary, - colors.secondary, - ], - ), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: colors.primary.withAlpha((255 * 0.3).round()), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - border: Border.all( - color: colors.secondary, - width: 2.5, - ), - ), - child: Icon( - Icons.emoji_events_rounded, - color: colors.onPrimary, - size: 20, + ), + if (isCompleted) + Padding( + padding: const EdgeInsets.only(left: 8), + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [colors.primary, colors.secondary], ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: colors.primary.withAlpha((255 * 0.3).round()), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + border: Border.all(color: colors.secondary, width: 2.5), + ), + child: Icon( + Icons.emoji_events_rounded, + color: colors.onPrimary, + size: 20, ), ), - ], - ), + ), + ], ), - ); - } - + ), + ); +} - Widget _buildLogoutSection(BuildContext context, ColorScheme colors, TextTheme textTheme) { - return FilledButton.icon( - icon: const Icon(Icons.logout_rounded), - label: const Text('Logout'), - onPressed: () { - context.read().add(ApiAuthLogoutRequested()); - context.go('/'); - }, - style: FilledButton.styleFrom( - backgroundColor: colors.error, - foregroundColor: colors.onError, - minimumSize: const Size.fromHeight(52), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - ), - textStyle: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ); - } +Widget _buildLogoutSection( + BuildContext context, + ColorScheme colors, + TextTheme textTheme, +) { + return FilledButton.icon( + icon: const Icon(Icons.logout_rounded), + label: const Text('Logout'), + onPressed: () { + context.read().add(AuthLogoutRequested()); + context.go('/'); + }, + style: FilledButton.styleFrom( + backgroundColor: colors.error, + foregroundColor: colors.onError, + minimumSize: const Size.fromHeight(52), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)), + textStyle: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ); } diff --git a/lib/features/statistics/bloc/api_statistics_bloc.dart b/lib/features/statistics/bloc/api_statistics_bloc.dart index c50f99e..fc7c5e6 100644 --- a/lib/features/statistics/bloc/api_statistics_bloc.dart +++ b/lib/features/statistics/bloc/api_statistics_bloc.dart @@ -1,18 +1,16 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; -import 'package:ketchapp_flutter/features/auth/bloc/api_auth_bloc.dart'; + import 'package:ketchapp_flutter/features/statistics/bloc/statistics_event.dart'; import 'package:ketchapp_flutter/features/statistics/bloc/statistics_state.dart'; import '../../../services/api_service.dart'; class ApiStatisticsBloc extends Bloc { - final ApiAuthBloc _apiAuthBloc; final ApiService _apiService; - ApiStatisticsBloc({required ApiAuthBloc apiAuthBloc, required ApiService apiService}) - : _apiAuthBloc = apiAuthBloc, - _apiService = apiService, - super(StatisticsState.initial()) { + ApiStatisticsBloc({required ApiService apiService}) + : _apiService = apiService, + super(StatisticsState.initial()) { on(_onLoadRequested); on(_onPreviousWeekRequested); on(_onNextWeekRequested); @@ -27,16 +25,6 @@ class ApiStatisticsBloc extends Bloc { ) async { emit(state.copyWith(status: StatisticsStatus.loading)); try { - final authState = _apiAuthBloc.state; - if (authState is! ApiAuthenticated) { - emit(state.copyWith( - status: StatisticsStatus.error, - errorMessage: 'User not authenticated')); - return; - } - - final userUuid = authState.userData['uuid']; - final formattedDate = "${date.year.toString().padLeft(4, '0')}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}"; @@ -47,8 +35,10 @@ class ApiStatisticsBloc extends Bloc { final formattedEnd = "${endOfWeek.year.toString().padLeft(4, '0')}-${endOfWeek.month.toString().padLeft(2, '0')}-${endOfWeek.day.toString().padLeft(2, '0')}"; - final url = "users/$userUuid/statistics?startDate=$formattedStart&endDate=$formattedEnd"; - final response = await _apiService.fetchData(url); + final response = await _apiService.getUserStatistics( + startDate: DateTime.parse(formattedStart), + endDate: DateTime.parse(formattedEnd), + ); if (response is Map && response.containsKey('dates')) { final dates = response['dates'] as List?; @@ -60,23 +50,30 @@ class ApiStatisticsBloc extends Bloc { List subjectStatsForDay = []; if (dayData != null && dayData['subjects'] is List) { - final tomatoes = (dayData['tomatoes'] ?? dayData['sessions'] ?? []) as List; + final tomatoes = + (dayData['tomatoes'] ?? dayData['sessions'] ?? []) + as List; final Map> subjectTomatoes = {}; for (final tomato in tomatoes) { - final subject = tomato['subject'] ?? tomato['subjectName'] ?? tomato['name']; + final subject = + tomato['subject'] ?? tomato['subjectName'] ?? tomato['name']; final id = tomato['id']; if (subject != null && id != null) { subjectTomatoes.putIfAbsent(subject, () => []).add(id); } } - subjectStatsForDay = (dayData['subjects'] as List).map((subject) { - final subjectMap = Map.from(subject as Map); - final subjectName = subjectMap['name'] ?? subjectMap['subject'] ?? subjectMap['subjectName']; - return { - ...subjectMap, - 'tomatoes': subjectTomatoes[subjectName] ?? [], - }; - }).toList(); + subjectStatsForDay = + (dayData['subjects'] as List).map((subject) { + final subjectMap = Map.from(subject as Map); + final subjectName = + subjectMap['name'] ?? + subjectMap['subject'] ?? + subjectMap['subjectName']; + return { + ...subjectMap, + 'tomatoes': subjectTomatoes[subjectName] ?? [], + }; + }).toList(); } final startOfWeek = date.subtract(Duration(days: date.weekday - 1)); @@ -88,8 +85,7 @@ class ApiStatisticsBloc extends Bloc { final dayDataInWeek = dates.firstWhere( (d) => - d is Map && - d['date'] == formattedDateInWeek, + d is Map && d['date'] == formattedDateInWeek, orElse: () => null, ); @@ -98,31 +94,41 @@ class ApiStatisticsBloc extends Bloc { } } - emit(state.copyWith( - status: StatisticsStatus.loaded, - displayedCalendarDate: date, - subjectStats: subjectStatsForDay, - weeklyStudyData: weeklyData, - weeklyDatesData: dates, - )); + emit( + state.copyWith( + status: StatisticsStatus.loaded, + displayedCalendarDate: date, + subjectStats: subjectStatsForDay, + weeklyStudyData: weeklyData, + weeklyDatesData: dates, + ), + ); } else { - emit(state.copyWith( - status: StatisticsStatus.error, - errorMessage: - "Formato di risposta API imprevisto ('dates' non è una lista).", - )); + emit( + state.copyWith( + status: StatisticsStatus.error, + errorMessage: + "Formato di risposta API imprevisto ('dates' non è una lista).", + ), + ); } } else { - emit(state.copyWith( - status: StatisticsStatus.loaded, - displayedCalendarDate: date, - subjectStats: [], - weeklyStudyData: List.filled(7, 0.0), - )); + emit( + state.copyWith( + status: StatisticsStatus.loaded, + displayedCalendarDate: date, + subjectStats: [], + weeklyStudyData: List.filled(7, 0.0), + ), + ); } } catch (e) { - emit(state.copyWith( - status: StatisticsStatus.error, errorMessage: e.toString())); + emit( + state.copyWith( + status: StatisticsStatus.error, + errorMessage: e.toString(), + ), + ); } } @@ -137,7 +143,9 @@ class ApiStatisticsBloc extends Bloc { StatisticsPreviousWeekRequested event, Emitter emit, ) async { - final newDate = state.displayedCalendarDate.subtract(const Duration(days: 7)); + final newDate = state.displayedCalendarDate.subtract( + const Duration(days: 7), + ); await _fetchAndEmitStatisticsForWeekContainingDate(newDate, emit); } @@ -163,7 +171,9 @@ class ApiStatisticsBloc extends Bloc { final selectedDate = event.selectedDate; final currentDisplayedDate = state.displayedCalendarDate; - final startOfWeek = currentDisplayedDate.subtract(Duration(days: currentDisplayedDate.weekday - 1)); + final startOfWeek = currentDisplayedDate.subtract( + Duration(days: currentDisplayedDate.weekday - 1), + ); final endOfWeek = startOfWeek.add(const Duration(days: 6)); if (selectedDate.isAfter(startOfWeek.subtract(const Duration(days: 1))) && @@ -179,29 +189,37 @@ class ApiStatisticsBloc extends Bloc { List subjectStatsForDay = []; if (dayData != null && dayData['subjects'] is List) { - final tomatoes = (dayData['tomatoes'] ?? dayData['sessions'] ?? []) as List; + final tomatoes = + (dayData['tomatoes'] ?? dayData['sessions'] ?? []) as List; final Map> subjectTomatoes = {}; for (final tomato in tomatoes) { - final subject = tomato['subject'] ?? tomato['subjectName'] ?? tomato['name']; + final subject = + tomato['subject'] ?? tomato['subjectName'] ?? tomato['name']; final id = tomato['id']; if (subject != null && id != null) { subjectTomatoes.putIfAbsent(subject, () => []).add(id); } } - subjectStatsForDay = (dayData['subjects'] as List).map((subject) { - final subjectMap = Map.from(subject as Map); - final subjectName = subjectMap['name'] ?? subjectMap['subject'] ?? subjectMap['subjectName']; - return { - ...subjectMap, - 'tomatoes': subjectTomatoes[subjectName] ?? [], - }; - }).toList(); + subjectStatsForDay = + (dayData['subjects'] as List).map((subject) { + final subjectMap = Map.from(subject as Map); + final subjectName = + subjectMap['name'] ?? + subjectMap['subject'] ?? + subjectMap['subjectName']; + return { + ...subjectMap, + 'tomatoes': subjectTomatoes[subjectName] ?? [], + }; + }).toList(); } - emit(state.copyWith( - displayedCalendarDate: selectedDate, - subjectStats: subjectStatsForDay, - )); + emit( + state.copyWith( + displayedCalendarDate: selectedDate, + subjectStats: subjectStatsForDay, + ), + ); } else { await _fetchAndEmitStatisticsForWeekContainingDate(selectedDate, emit); } @@ -212,11 +230,13 @@ class ApiStatisticsBloc extends Bloc { Emitter emit, ) async { if (event.newTotalStudyHours > state.recordStudyHours) { - emit(state.copyWith( - recordStudyHours: event.newTotalStudyHours, - bestStudyDay: DateTime.now(), - status: StatisticsStatus.loaded, - )); + emit( + state.copyWith( + recordStudyHours: event.newTotalStudyHours, + bestStudyDay: DateTime.now(), + status: StatisticsStatus.loaded, + ), + ); } else if (state.status != StatisticsStatus.loaded) { emit(state.copyWith(status: StatisticsStatus.loaded)); } diff --git a/lib/features/timer/presentation/timer_page.dart b/lib/features/timer/presentation/timer_page.dart index 10a6bcb..827e97b 100644 --- a/lib/features/timer/presentation/timer_page.dart +++ b/lib/features/timer/presentation/timer_page.dart @@ -511,7 +511,6 @@ class _TimerViewState extends State with TickerProviderStateMixin { ); } - Future _toggleWhiteNoise(bool enable) async { if (enable) { _audioPlayer ??= AudioPlayer(); @@ -532,7 +531,6 @@ class _TimerViewState extends State with TickerProviderStateMixin { } } - int _getPomodoroDuration() { return 25 * 60; } @@ -563,10 +561,10 @@ class _TimerViewState extends State with TickerProviderStateMixin { create: (context) { final apiService = Provider.of(context, listen: false); final authState = context.read().state; - if (authState is Authenticated) { + if (authState is AuthAuthenticated) { final bloc = TimerBloc( apiService: apiService, - userUUID: authState.userUuid, + userUUID: authState.id, tomatoId: widget.tomatoes[_currentTomatoIndex].id, ); bloc.add( @@ -648,7 +646,6 @@ class _TimerViewState extends State with TickerProviderStateMixin { } void _handleTimerStateChanges(BuildContext context, TimerState state) { - switch (state.runtimeType) { case const (WaitingNextTomato): _handleNextTomato(state as WaitingNextTomato); @@ -1199,9 +1196,7 @@ class _TimerViewState extends State with TickerProviderStateMixin { ? colors.tertiaryContainer : colors.primaryContainer) .withValues(alpha: 0.3), - borderRadius: BorderRadius.circular( - 3, - ), + borderRadius: BorderRadius.circular(3), ), child: Text( 'remaining', @@ -1257,7 +1252,6 @@ class _TimerViewState extends State with TickerProviderStateMixin { ColorScheme colors, ThemeData theme, ) { - if (state is WaitingFirstTomato || state is WaitingNextTomato || state is TomatoTimerReady) { diff --git a/lib/features/welcome/presentation/pages/welcome_page.dart b/lib/features/welcome/presentation/pages/welcome_page.dart index ea64929..64e66b4 100644 --- a/lib/features/welcome/presentation/pages/welcome_page.dart +++ b/lib/features/welcome/presentation/pages/welcome_page.dart @@ -3,7 +3,6 @@ import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:introduction_screen/introduction_screen.dart'; - import './page_one_view.dart'; import './page_two_view.dart'; import './page_three_view.dart'; @@ -15,7 +14,8 @@ class WelcomePage extends StatefulWidget { _WelcomePageState createState() => _WelcomePageState(); } -class _WelcomePageState extends State with TickerProviderStateMixin { +class _WelcomePageState extends State + with TickerProviderStateMixin { final _introKey = GlobalKey(); late AnimationController _fadeAnimationController; @@ -46,21 +46,19 @@ class _WelcomePageState extends State with TickerProviderStateMixin vsync: this, ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _fadeAnimationController, - curve: Curves.easeOutCubic, - )); - - _scaleAnimation = Tween( - begin: 0.95, - end: 1.0, - ).animate(CurvedAnimation( - parent: _scaleAnimationController, - curve: Curves.easeOutBack, - )); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _fadeAnimationController, + curve: Curves.easeOutCubic, + ), + ); + + _scaleAnimation = Tween(begin: 0.95, end: 1.0).animate( + CurvedAnimation( + parent: _scaleAnimationController, + curve: Curves.easeOutBack, + ), + ); } @override @@ -76,14 +74,8 @@ class _WelcomePageState extends State with TickerProviderStateMixin final textTheme = Theme.of(context).textTheme; final List pages = [ - buildPageOneViewModel( - colors: colors, - primaryAccentColor: colors.primary, - ), - buildPageTwoViewModel( - colors: colors, - primaryAccentColor: colors.primary, - ), + buildPageOneViewModel(colors: colors, primaryAccentColor: colors.primary), + buildPageTwoViewModel(colors: colors, primaryAccentColor: colors.primary), buildPageThreeViewModel( colors: colors, primaryAccentColor: colors.primary, @@ -97,14 +89,16 @@ class _WelcomePageState extends State with TickerProviderStateMixin final SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle( statusBarColor: Colors.transparent, - statusBarIconBrightness: colors.brightness == Brightness.light - ? Brightness.dark - : Brightness.light, + statusBarIconBrightness: + colors.brightness == Brightness.light + ? Brightness.dark + : Brightness.light, statusBarBrightness: colors.brightness, systemNavigationBarColor: colors.surface, - systemNavigationBarIconBrightness: colors.brightness == Brightness.light - ? Brightness.dark - : Brightness.light, + systemNavigationBarIconBrightness: + colors.brightness == Brightness.light + ? Brightness.dark + : Brightness.light, ); return AnnotatedRegion( @@ -141,7 +135,9 @@ class _WelcomePageState extends State with TickerProviderStateMixin vertical: 12, ), decoration: BoxDecoration( - color: colors.surfaceContainerHighest.withValues(alpha: 0.5), + color: colors.surfaceContainerHighest.withValues( + alpha: 0.5, + ), borderRadius: BorderRadius.circular(25), border: Border.all( color: colors.outline.withValues(alpha: 0.3), @@ -195,13 +191,10 @@ class _WelcomePageState extends State with TickerProviderStateMixin ), ], ), - child: Text( - 'Start', - style: textTheme.titleMedium?.copyWith( - color: colors.onPrimary, - fontWeight: FontWeight.w700, - letterSpacing: 0.5, - ), + child: Icon( + Icons.arrow_forward_rounded, + color: colors.onPrimary, + size: 24, ), ), dotsDecorator: DotsDecorator( @@ -220,7 +213,12 @@ class _WelcomePageState extends State with TickerProviderStateMixin isProgressTap: false, freeze: false, bodyPadding: EdgeInsets.zero, - controlsPadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 48.0), + controlsPadding: const EdgeInsets.fromLTRB( + 24.0, + 24.0, + 24.0, + 48.0, + ), globalBackgroundColor: Colors.transparent, ), ), diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart deleted file mode 100644 index b654e14..0000000 --- a/lib/firebase_options.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; - -class DefaultFirebaseOptions { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - return web; - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - return macos; - case TargetPlatform.windows: - return windows; - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyBjG39IYX0pWkn6cHTLCEbXAU2C5pI21DE', - appId: '1:1049541862968:web:1ce1149572d42bd6d528cd', - messagingSenderId: '1049541862968', - projectId: 'testalebrutto', - authDomain: 'testalebrutto.firebaseapp.com', - storageBucket: 'testalebrutto.firebasestorage.app', - measurementId: 'G-6B1DP28B0T', - ); - - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyBMA14zU26YiyDeh-HlemBrCSVaoqbtiu0', - appId: '1:1049541862968:android:08618ab4c347426cd528cd', - messagingSenderId: '1049541862968', - projectId: 'testalebrutto', - storageBucket: 'testalebrutto.firebasestorage.app', - androidClientId: - '1049541862968-889r2k03phkb1c2gk6l75cobpid1k7m0.apps.googleusercontent.com', - ); - - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyBHOUd3jeH5HAgGpnEpUothkIa9UwrzNso', - appId: '1:1049541862968:ios:3cc3d77d12df491bd528cd', - messagingSenderId: '1049541862968', - projectId: 'testalebrutto', - storageBucket: 'testalebrutto.firebasestorage.app', - androidClientId: '1049541862968-889r2k03phkb1c2gk6l75cobpid1k7m0.apps.googleusercontent.com', - iosClientId: '1049541862968-fklmqnvph4ven6tmfci521n13h6bndqu.apps.googleusercontent.com', - iosBundleId: 'com.example.ketchappFlutter', - ); - - static const FirebaseOptions macos = FirebaseOptions( - apiKey: 'AIzaSyBHOUd3jeH5HAgGpnEpUothkIa9UwrzNso', - appId: '1:1049541862968:ios:3cc3d77d12df491bd528cd', - messagingSenderId: '1049541862968', - projectId: 'testalebrutto', - storageBucket: 'testalebrutto.firebasestorage.app', - androidClientId: '1049541862968-889r2k03phkb1c2gk6l75cobpid1k7m0.apps.googleusercontent.com', - iosClientId: '1049541862968-fklmqnvph4ven6tmfci521n13h6bndqu.apps.googleusercontent.com', - iosBundleId: 'com.example.ketchappFlutter', - ); - - static const FirebaseOptions windows = FirebaseOptions( - apiKey: 'AIzaSyBjG39IYX0pWkn6cHTLCEbXAU2C5pI21DE', - appId: '1:1049541862968:web:dce345b9f6c150ded528cd', - messagingSenderId: '1049541862968', - projectId: 'testalebrutto', - authDomain: 'testalebrutto.firebaseapp.com', - storageBucket: 'testalebrutto.firebasestorage.app', - measurementId: 'G-03YL3FXRV0', - ); - -} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 5d4152f..b621892 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:ketchapp_flutter/features/auth/bloc/api_auth_bloc.dart'; import 'package:ketchapp_flutter/features/profile/bloc/api_profile_bloc.dart'; -import 'package:ketchapp_flutter/services/api_auth_service.dart'; -import 'firebase_options.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:provider/provider.dart'; import 'package:ketchapp_flutter/features/auth/bloc/auth_bloc.dart'; @@ -20,39 +15,25 @@ Future main() async { usePathUrlStrategy(); await initializeDateFormatting('it_IT', null); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await NotificationService.initialize(); final apiService = ApiService(); - await apiService.loadToken(); // Assicurati che il token venga caricato all'avvio + await apiService + .loadToken(); // Assicurati che il token venga caricato all'avvio runApp( MultiProvider( providers: [ - Provider( - create: (_) => apiService, - ), - Provider( - create: (context) => ApiAuthService(context.read()), - ), + Provider(create: (_) => apiService), BlocProvider( - create: (context) => AuthBloc( - firebaseAuth: FirebaseAuth.instance, - apiService: context.read(), - ), - ), - BlocProvider( - create: (context) => ApiAuthBloc( - apiAuthService: context.read(), - apiService: context.read(), - )..add(ApiAuthCheckRequested()), // Controlla lo stato dell'auth all'avvio + create: (context) => AuthBloc(apiService: context.read()), ), BlocProvider( - create: (context) => ApiProfileBloc( - apiService: context.read(), - apiAuthBloc: context.read(), - imagePicker: ImagePicker(), - ), + create: + (context) => ApiProfileBloc( + apiService: context.read(), + imagePicker: ImagePicker(), + ), ), ], child: const MyApp(), diff --git a/lib/models/achievement.dart b/lib/models/achievement.dart index 22a3379..9021c0a 100644 --- a/lib/models/achievement.dart +++ b/lib/models/achievement.dart @@ -3,22 +3,27 @@ class Achievement { final String description; final DateTime? collectedDate; final String iconUrl; + final bool completed; Achievement({ required this.title, required this.description, this.collectedDate, required this.iconUrl, + required this.completed, }); factory Achievement.fromJson(Map json) { return Achievement( title: json['title']?.toString() ?? 'Unknown Achievement', - description: json['description']?.toString() ?? 'No description available', - collectedDate: json['collected_date'] != null - ? DateTime.parse(json['collected_date']) - : null, + description: + json['description']?.toString() ?? 'No description available', + collectedDate: + json['collected_date'] != null + ? DateTime.parse(json['collected_date']) + : null, iconUrl: json['icon_url']?.toString() ?? '', + completed: json['completed'] == true, ); } } diff --git a/lib/services/api_auth_service.dart b/lib/services/api_auth_service.dart deleted file mode 100644 index 573d1d6..0000000 --- a/lib/services/api_auth_service.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:ketchapp_flutter/services/api_service.dart'; - -class ApiAuthService { - final String _authBaseUrl = "http://151.61.228.91:8080/api"; - final ApiService _apiService; - - ApiAuthService(this._apiService); - - Future> login(String username, String password) async { - final response = await _apiService.postData( - 'auth/login', - { - 'username': username, - 'password': password, - }, - baseUrlOverride: _authBaseUrl, - ); - return response; - } - - Future register({ - required String username, - required String email, - required String password, - }) async { - await _apiService.postData( - 'auth/register', - { - 'username': username, - 'email': email, - 'password': password, - }, - baseUrlOverride: _authBaseUrl, - ); - } - - Future logout() async { - // Assumendo un endpoint di logout che invalida il token sul server - await _apiService.postData('auth/logout', {}); - // Qui dovresti cancellare il token salvato localmente. - } - - Future sendPasswordResetEmail(String email) async { - await _apiService.postData('auth/request-password-reset', { - 'email': email, - }); - } - - // Potrebbe essere necessario un metodo per ottenere lo stato di autenticazione - // controllando la validità del token salvato. - Future isAuthenticated() async { - // Esempio: controlla se un token è salvato e non è scaduto. - // Potresti anche avere un endpoint /auth/me o /auth/user per verificarlo. - try { - await _apiService.fetchData('auth/me'); - return true; - } catch (e) { - return false; - } - } -} diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 8aab132..7feca01 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -5,15 +5,13 @@ import 'package:ketchapp_flutter/models/tomato.dart'; import 'package:ketchapp_flutter/models/activity_action.dart'; import 'package:ketchapp_flutter/models/achievement.dart'; import './api_exceptions.dart'; -import 'package:ketchapp_flutter/services/calendar_service.dart'; import 'package:ketchapp_flutter/services/notification_service.dart'; import 'package:ketchapp_flutter/models/activity.dart'; import 'package:ketchapp_flutter/models/activity_type.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:io'; class ApiService { - final String _baseUrl = "http://151.61.228.91:8080/api"; + final String _baseUrl = "http://localhost:8080/api"; String? _token; ApiService() { @@ -44,6 +42,7 @@ class ApiService { 'Content-Type': 'application/json; charset=UTF-8', }; if (_token != null) { + print('Auth Token: $_token'); headers['Authorization'] = 'Bearer $_token'; } return headers; @@ -63,33 +62,65 @@ class ApiService { case 201: return Future.value(decodedJson); case 400: - throw BadRequestException(decodedJson is Map ? decodedJson['message'] ?? 'Richiesta non valida' : 'Richiesta non valida'); + throw BadRequestException( + decodedJson is Map + ? decodedJson['message'] ?? 'Richiesta non valida' + : 'Richiesta non valida', + ); case 401: - throw UnauthorizedException(decodedJson is Map ? decodedJson['message'] ?? 'Non autorizzato' : 'Non autorizzato'); + throw UnauthorizedException( + decodedJson is Map + ? decodedJson['message'] ?? 'Non autorizzato' + : 'Non autorizzato', + ); case 403: - throw ForbiddenException(decodedJson is Map ? decodedJson['message'] ?? 'Accesso negato' : 'Accesso negato'); + throw ForbiddenException( + decodedJson is Map + ? decodedJson['message'] ?? 'Accesso negato' + : 'Accesso negato', + ); case 404: - throw NotFoundException(decodedJson is Map ? decodedJson['message'] ?? 'Risorsa non trovata' : 'Risorsa non trovata'); + throw NotFoundException( + decodedJson is Map + ? decodedJson['message'] ?? 'Risorsa non trovata' + : 'Risorsa non trovata', + ); case 409: - throw UserAlreadyExistsException(decodedJson is Map ? decodedJson['message'] ?? 'L\'utente esiste già.' : 'L\'utente esiste già.'); + throw UserAlreadyExistsException( + decodedJson is Map + ? decodedJson['message'] ?? 'L\'utente esiste già.' + : 'L\'utente esiste già.', + ); case 500: - throw InternalServerErrorException(decodedJson is Map ? decodedJson['message'] ?? 'Errore interno del server' : 'Errore interno del server'); + throw InternalServerErrorException( + decodedJson is Map + ? decodedJson['message'] ?? 'Errore interno del server' + : 'Errore interno del server', + ); default: throw FetchDataException( - 'Error occurred while communicating with server with status code: ${response.statusCode}'); + 'Error occurred while communicating with server with status code: ${response.statusCode}', + ); } } Future fetchData(String endpoint, {String? baseUrlOverride}) async { + if (_token == null) { + await loadToken(); + } final url = (baseUrlOverride ?? _baseUrl) + '/$endpoint'; - final response = await http.get( - Uri.parse(url), - headers: _getHeaders(), - ); + final response = await http.get(Uri.parse(url), headers: _getHeaders()); return _processResponse(response); } - Future postData(String endpoint, Map data, {String? baseUrlOverride}) async { + Future postData( + String endpoint, + Map data, { + String? baseUrlOverride, + }) async { + if (_token == null) { + await loadToken(); + } final url = (baseUrlOverride ?? _baseUrl) + '/$endpoint'; final response = await http.post( Uri.parse(url), @@ -101,6 +132,9 @@ class ApiService { } Future deleteData(String endpoint) async { + if (_token == null) { + await loadToken(); + } final response = await http.delete( Uri.parse('$_baseUrl/$endpoint'), headers: _getHeaders(), @@ -109,14 +143,18 @@ class ApiService { } Future findEmailByUsername(String username) async { - final response = await http.get(Uri.parse('$_baseUrl/users/email/$username'), headers: _getHeaders()); + final response = await http.get( + Uri.parse('$_baseUrl/users/email/$username'), + headers: _getHeaders(), + ); return _processResponse(response); } Future getUserUUIDByFirebaseUid(String firebaseUid) async { final response = await http.get( - Uri.parse('$_baseUrl/users/firebase/$firebaseUid'), - headers: _getHeaders()); + Uri.parse('$_baseUrl/users/firebase/$firebaseUid'), + headers: _getHeaders(), + ); final responseData = await _processResponse(response); return responseData.toString(); } @@ -135,15 +173,13 @@ class ApiService { final endAt = tomato['end_at']; if (startAt != null && endAt != null) { final start = DateTime.parse(startAt); - final notificationStart = start.subtract(const Duration(minutes: 15)); - final end = DateTime.parse(endAt); - await CalendarService().addEvent( - title: subjectName, - start: start, - end: end, - description: 'Sessione Pomodoro creata da KetchApp', + final notificationStart = start.subtract( + const Duration(minutes: 15), + ); + await NotificationService.schedulePomodoroNotification( + subjectName, + notificationStart, ); - await NotificationService.schedulePomodoroNotification(subjectName, notificationStart); } } } @@ -154,31 +190,19 @@ class ApiService { } } - Future> getUserByFirebaseUid(String firebaseUid) async { - try { - final response = await fetchData('users/firebase/$firebaseUid'); - return response as Map; - } catch (e) { - rethrow; - } - } - Future> getUsersForRanking() async { - final response = await fetchData("users"); + final response = await fetchData("users"); return response as List; } - Future getGlobalRanking() async { - final response = await http.get(Uri.parse('$_baseUrl/users/ranking/global'), headers: _getHeaders()); - return _processResponse(response); - } - - Future> getTodaysTomatoes(String userUuid) async { + Future> getTodaysTomatoes() async { final today = DateTime.now().toUtc(); - final todayStr = "${today.year.toString().padLeft(4, '0')}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}"; - final response = await fetchData('users/$userUuid/tomatoes?date=$todayStr'); + final todayStr = + "${today.year.toString().padLeft(4, '0')}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}"; + final response = await fetchData('users/@me/tomatoes?date=$todayStr'); final List tomatoesJson = response as List; - List tomatoes = tomatoesJson.map((json) => Tomato.fromJson(json)).toList(); + List tomatoes = + tomatoesJson.map((json) => Tomato.fromJson(json)).toList(); for (var tomato in tomatoes) { tomato.activities = await getTomatoActivities(tomato.id); @@ -193,22 +217,17 @@ class ApiService { return activitiesJson.map((json) => Activity.fromJson(json)).toList(); } - Future> getUserAchievements(String userUuid) async { - final response = await http.get(Uri.parse('$_baseUrl/users/$userUuid/achievements'), headers: _getHeaders()); + Future> getUserAchievements() async { + final response = await http.get( + Uri.parse('$_baseUrl/users/@me/achievements'), + headers: _getHeaders(), + ); final decodedJson = await _processResponse(response); return (decodedJson as List).map((e) => Achievement.fromJson(e)).toList(); } - - Future> getAllAchievements() async { - final response = await fetchData('achievements'); - return response as List; - } - - - Future> getAchievements(String userUuid) async { - final response = await fetchData('users/$userUuid/achievements'); - return response as List; + Future getCurrentUser() async { + return await fetchData('users/@me'); } Future getTomatoById(int tomatoId) async { @@ -216,8 +235,31 @@ class ApiService { return Tomato.fromJson(response); } + String _formatDate(DateTime date) { + return "${date.year.toString().padLeft(4, '0')}-" + "${date.month.toString().padLeft(2, '0')}-" + "${date.day.toString().padLeft(2, '0')}"; + } + + Future getUserStatistics({ + required DateTime startDate, + required DateTime endDate, + }) async { + final startDateStr = _formatDate(startDate); + final endDateStr = _formatDate(endDate); + + final response = await fetchData( + 'users/@me/statistics?startDate=$startDateStr&endDate=$endDateStr', + ); + return response; + } + Future createActivity( - String userUUID, int tomatoId, ActivityAction action, ActivityType type) async { + String userUUID, + int tomatoId, + ActivityAction action, + ActivityType type, + ) async { try { await postData('activities', { 'userUUID': userUUID, @@ -240,24 +282,4 @@ class ApiService { } return chain; } - - Future> uploadProfilePicture(String userUuid, File imageFile) async { - final request = http.MultipartRequest( - 'POST', - Uri.parse('$_baseUrl/users/$userUuid/profile-picture'), - ); - request.headers.addAll(_getHeaders()); - request.files.add(await http.MultipartFile.fromPath('profilePicture', imageFile.path)); - - final response = await request.send(); - return await _processResponse(await http.Response.fromStream(response)) as Map; - } - - Future> deleteProfilePicture(String userUuid) async { - final response = await http.delete( - Uri.parse('$_baseUrl/users/$userUuid/profile-picture'), - headers: _getHeaders(), - ); - return await _processResponse(response) as Map; - } } diff --git a/lib/services/calendar_service.dart b/lib/services/calendar_service.dart deleted file mode 100644 index 5ef0381..0000000 --- a/lib/services/calendar_service.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:google_sign_in/google_sign_in.dart'; -import 'package:googleapis/calendar/v3.dart' as cal; -import 'package:googleapis_auth/googleapis_auth.dart' as gapi_auth; -import 'package:http/http.dart' as http; - -const List _calendarScopes = [ - cal.CalendarApi.calendarScope, -]; - -class GoogleHttpClient extends http.BaseClient { - final Map _headers; - final http.Client _client = http.Client(); - - GoogleHttpClient(this._headers); - - @override - Future send(http.BaseRequest request) { - request.headers.addAll(_headers); - return _client.send(request); - } -} - -class CalendarService { - final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: _calendarScopes); - - Future getCalendarApi() async { - final GoogleSignInAccount? googleUser = await _googleSignIn.signInSilently() ?? await _googleSignIn.signIn(); - if (googleUser == null) { - return null; - } - - gapi_auth.AccessCredentials? credentials; - try { - final GoogleSignInAuthentication googleAuth = await googleUser.authentication; - if (googleAuth.accessToken == null) { - throw Exception('Google Sign-In access token is null'); - } - credentials = gapi_auth.AccessCredentials( - gapi_auth.AccessToken( - "Bearer", - googleAuth.accessToken!, - DateTime.now().toUtc().add(const Duration(minutes: 55)), - ), - null, - _calendarScopes, - ); - } catch (e) { - return null; - } - - final httpClient = gapi_auth.authenticatedClient( - http.Client(), - credentials, - closeUnderlyingClient: true, - ); - - return cal.CalendarApi(httpClient); - } - - Future> getEvents() async { - final calendarApi = await getCalendarApi(); - if (calendarApi == null) { - return []; - } - - try { - final now = DateTime.now(); - final timeMin = DateTime(now.year, now.month, 1).toUtc(); - final timeMax = DateTime(now.year, now.month + 1, 1).toUtc(); - - final cal.Events eventsResult = await calendarApi.events.list( - 'primary', - timeMin: timeMin, - timeMax: timeMax, - singleEvents: true, - orderBy: 'startTime', - ); - return eventsResult.items ?? []; - } catch (e) { - if (e is cal.DetailedApiRequestError) { - } else { - } - return []; - } - } - - Future addEvent({ - required String title, - required DateTime start, - required DateTime end, - String? description, - }) async { - final calendarApi = await getCalendarApi(); - if (calendarApi == null) { - return; - } - } -} - diff --git a/pubspec.lock b/pubspec.lock index 665e3fa..d98356f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" audioplayers: dependency: "direct main" description: @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -649,10 +649,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" introduction_screen: dependency: "direct main" description: @@ -673,10 +673,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -1118,10 +1118,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3beb9ee..4f68fc0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: # Utils uuid: ^4.5.1 - intl: 0.19.0 + intl: ^0.20.2 # Google APIs per Google Calendar googleapis: ^14.0.0