import 'package:flutter/material.dart'; import '../admin/widgets/admin_app_bar_action.dart'; import '../models/app_user.dart'; import '../models/guess_score_summary.dart'; import '../models/incoming_question.dart'; import '../utils/guess_slot_format.dart'; import '../models/sync_result.dart'; import '../models/user_profile.dart'; import '../repositories/user_profile_repository.dart'; import '../services/auth_service.dart'; import '../services/questions_hub_service.dart'; import '../theme/app_theme.dart'; import '../widgets/profile_avatar.dart'; import '../widgets/swipe_question_tile.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({ super.key, required this.user, this.profile, this.syncStatus = ProfileSyncStatus.idle, }); final AppUser user; final UserProfile? profile; final ProfileSyncStatus syncStatus; @override Widget build(BuildContext context) { final String? photoUrl = profile?.photoUrl ?? user.photoUrl; final String displayName = profile?.displayName ?? user.displayName ?? 'there'; final String? email = profile?.email ?? user.email; final Color syncIconColor = switch (syncStatus) { ProfileSyncStatus.syncing => Colors.white, ProfileSyncStatus.synced => AppColors.success, ProfileSyncStatus.error => Colors.redAccent, ProfileSyncStatus.offline => Colors.orange, ProfileSyncStatus.idle => AppColors.textSecondary, }; return Stack( children: [ Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, centerTitle: true, title: ListenableBuilder( listenable: Listenable.merge([ QuestionsHubService.instance.hasPendingQuestion, QuestionsHubService.instance.pendingQuestion, QuestionsHubService.instance.pendingQuestionCount, QuestionsHubService.instance.guessScoreSummary, ]), builder: (BuildContext context, Widget? child) { final int count = QuestionsHubService.instance.pendingQuestionCount.value; final bool hasPending = QuestionsHubService.instance.hasPendingQuestion.value; final GuessScoreSummary score = QuestionsHubService.instance.guessScoreSummary.value; return Row( mainAxisSize: MainAxisSize.min, children: [ _CumulativeScoreChip( summary: score, onPressed: () => _showGuessScoreStatsDialog(context, score), ), if (hasPending && count >= 1) ...[ const SizedBox(width: 8), _QuestionEnvelopeButton( count: count, onPressed: () => QuestionsHubService.instance.openQuestionPanel(), ), ], ], ); }, ), actions: [ const AdminAppBarAction(), IconButton( onPressed: () => UserProfileRepository.instance.sync(), tooltip: 'Sync profile', icon: Icon(Icons.sync, color: syncIconColor), ), IconButton( onPressed: () => AuthService.instance.signOut(), tooltip: 'Sign out', icon: const Icon(Icons.logout), ), ], ), body: DecoratedBox( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [AppColors.background, AppColors.surface], ), ), child: SafeArea( child: ListenableBuilder( listenable: Listenable.merge([ QuestionsHubService.instance.questionPanelOpen, QuestionsHubService.instance.hasPendingQuestion, QuestionsHubService.instance.questionQueue, QuestionsHubService.instance.pendingQuestion, QuestionsHubService.instance.questionActionBusy, QuestionsHubService.instance.pendingQuestionCount, ]), builder: (BuildContext context, Widget? child) { final QuestionsHubService hub = QuestionsHubService.instance; final bool panelOpen = hub.questionPanelOpen.value; final bool hasPending = hub.hasPendingQuestion.value; final IncomingQuestion? question = hub.currentQuestion; final bool showQuestionPanel = panelOpen && hasPending && question != null; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (showQuestionPanel) Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(8, 4, 8, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ const Spacer(), IconButton( onPressed: hub.closeQuestionPanel, tooltip: 'Close', icon: const Icon(Icons.close), ), ], ), const SizedBox(height: 4), Expanded( child: SwipeQuestionTile( key: ValueKey(question.id), questionId: question.id, busy: hub.questionActionBusy.value, onSwipeRight: (num answer) => hub.submitCurrentAnswer(answer: answer), onSwipeLeft: hub.deferCurrentQuestion, ), ), ], ), ), ), if (!showQuestionPanel) Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: AppColors.surfaceElevated, borderRadius: BorderRadius.circular(16), border: Border.all( color: AppColors.accent.withValues(alpha: 0.2), ), ), child: Column( children: [ ProfileAvatar( photoUrl: photoUrl, radius: 36, ), const SizedBox(height: 16), Text( 'Welcome, $displayName', style: Theme.of(context) .textTheme .headlineMedium, textAlign: TextAlign.center, ), if (email != null) ...[ const SizedBox(height: 8), Text( email, style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), ], const SizedBox(height: 12), _SyncStatusChip(status: syncStatus), const SizedBox(height: 20), const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.check_circle, color: AppColors.success, size: 20, ), SizedBox(width: 8), Text( 'You\'re signed in', style: TextStyle( color: AppColors.success, fontWeight: FontWeight.w600, ), ), ], ), ], ), ), const SizedBox(height: 24), Text( profile?.dirty == true ? 'Profile saved locally. Will sync when online.' : 'Profile synced with your account.', style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), if (UserProfileRepository.instance.usesLocalStore) ...[ const SizedBox(height: 16), OutlinedButton.icon( onPressed: profile == null ? null : () async { final UserProfile current = profile!; await UserProfileRepository.instance .updateProfile( current.copyWith( onboardingCompleted: !current .onboardingCompleted, ), ); }, icon: const Icon(Icons.toggle_on_outlined), label: Text( profile?.onboardingCompleted == true ? 'Mark onboarding incomplete' : 'Mark onboarding complete', ), ), ], ], ), ), ), ], ); }, ), ), ), ), ListenableBuilder( listenable: QuestionsHubService.instance.scoreResetBusy, builder: (BuildContext context, Widget? child) { if (!QuestionsHubService.instance.scoreResetBusy.value) { return const SizedBox.shrink(); } return Stack( key: const Key('guess-score-reset-blocking-overlay'), children: [ ModalBarrier( dismissible: false, color: Colors.black.withValues(alpha: 0.54), ), const Center( child: CircularProgressIndicator(color: AppColors.accent), ), ], ); }, ), ], ); } } class _QuestionEnvelopeButton extends StatelessWidget { const _QuestionEnvelopeButton({ required this.count, required this.onPressed, }); final int count; final VoidCallback onPressed; @override Widget build(BuildContext context) { return Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ IconButton( onPressed: onPressed, tooltip: count == 1 ? '1 question' : '$count questions', icon: const Icon(Icons.mail_outline, size: 22), style: IconButton.styleFrom( backgroundColor: AppColors.accent.withValues(alpha: 0.15), foregroundColor: AppColors.accent, padding: const EdgeInsets.all(8), minimumSize: const Size(36, 36), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), if (count >= 1) Positioned( top: 4, right: 0, child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( color: AppColors.accent, borderRadius: BorderRadius.circular(10), border: Border.all(color: AppColors.surface, width: 1.5), ), constraints: const BoxConstraints(minWidth: 18, minHeight: 18), child: Text( '$count', textAlign: TextAlign.center, style: const TextStyle( color: AppColors.background, fontSize: 11, fontWeight: FontWeight.w700, height: 1.1, ), ), ), ), ], ); } } void _showGuessScoreStatsDialog(BuildContext context, GuessScoreSummary summary) { final String totalText = _formatScoreNumber(summary.total); final String percentText = _formatScoreNumber(summary.percentCorrect); final String? slotText = summary.slotStart == null ? null : formatGuessSlotRange( slotStart: summary.slotStart!, newerSlotStart: summary.newerSlotStart, ); showDialog( context: context, builder: (BuildContext dialogContext) { return AlertDialog( key: const Key('guess-score-stats-dialog'), title: const Text('Score statistics'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (slotText != null) _ScoreStatRow( key: const Key('guess-score-slot-row'), label: 'Time slot', value: slotText, ), _ScoreStatRow(label: 'Total score', value: totalText), _ScoreStatRow( label: 'Questions answered', value: '${summary.answersTotal}', ), _ScoreStatRow(label: 'Percent correct', value: '$percentText%'), ], ), actions: [ TextButton( key: const Key('guess-score-reset-button'), onPressed: () => _confirmResetGuessScore(dialogContext), child: const Text('Reset'), ), TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Close'), ), ], ); }, ); } String _formatScoreNumber(num value) { return value == value.roundToDouble() ? value.toStringAsFixed(0) : value.toStringAsFixed(2); } Future _confirmResetGuessScore(BuildContext context) async { final bool? confirmed = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( key: const Key('guess-score-reset-confirm-dialog'), title: const Text('Reset score?'), content: const Text( 'This clears your total score and answer statistics. ' 'You cannot undo this.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), TextButton( key: const Key('guess-score-reset-confirm-button'), onPressed: () => Navigator.of(context).pop(true), child: const Text('Reset'), ), ], ); }, ); if (confirmed != true || !context.mounted) { return; } Navigator.of(context).pop(); await QuestionsHubService.instance.resetGuessScoreSummary(); } class _ScoreStatRow extends StatelessWidget { const _ScoreStatRow({ super.key, required this.label, required this.value, }); final String label; final String value; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: Theme.of(context).textTheme.bodyLarge), Text( value, style: Theme.of(context).textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w700, color: AppColors.accent, ), ), ], ), ); } } class _CumulativeScoreChip extends StatelessWidget { const _CumulativeScoreChip({ required this.summary, required this.onPressed, }); final GuessScoreSummary summary; final VoidCallback onPressed; @override Widget build(BuildContext context) { final String scoreText = summary.total == summary.total.roundToDouble() ? summary.total.toStringAsFixed(0) : summary.total.toStringAsFixed(2); return Material( color: Colors.transparent, child: InkWell( key: const Key('topbar-cumulative-score'), onTap: onPressed, borderRadius: BorderRadius.circular(999), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: AppColors.accent.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(999), border: Border.all(color: AppColors.accent.withValues(alpha: 0.35)), ), child: Text( 'Score: $scoreText', style: const TextStyle( color: AppColors.accent, fontWeight: FontWeight.w700, fontSize: 12, ), ), ), ), ); } } class _SyncStatusChip extends StatelessWidget { const _SyncStatusChip({required this.status}); final ProfileSyncStatus status; @override Widget build(BuildContext context) { final ({String label, Color color, IconData icon}) style = switch (status) { ProfileSyncStatus.syncing => ( label: 'Syncing…', color: AppColors.accent, icon: Icons.sync, ), ProfileSyncStatus.synced => ( label: 'Synced', color: AppColors.success, icon: Icons.cloud_done_outlined, ), ProfileSyncStatus.offline => ( label: 'Offline', color: Colors.orange, icon: Icons.cloud_off_outlined, ), ProfileSyncStatus.error => ( label: 'Sync error', color: Colors.redAccent, icon: Icons.error_outline, ), ProfileSyncStatus.idle => ( label: 'Ready', color: AppColors.accent, icon: Icons.cloud_queue_outlined, ), }; return Chip( avatar: Icon(style.icon, size: 18, color: style.color), label: Text(style.label), side: BorderSide(color: style.color.withValues(alpha: 0.4)), ); } }