import 'package:flutter/material.dart'; import '../admin/widgets/admin_app_bar_action.dart'; import '../models/app_user.dart'; import '../models/incoming_question.dart'; import '../models/question_source_preference.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/hub_toolbar_icon_button.dart'; import '../widgets/profile_avatar.dart'; import '../widgets/question_source_settings_sheet.dart'; import '../repositories/question_source_config_repository.dart'; import '../widgets/question_presentation_router.dart'; import '../widgets/market_question_placeholder.dart'; import '../widgets/spelling_question_placeholder.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, elevation: 0, scrolledUnderElevation: 0, automaticallyImplyLeading: false, titleSpacing: 8, centerTitle: false, title: ListenableBuilder( listenable: Listenable.merge([ QuestionsHubService.instance.hasPendingQuestion, QuestionsHubService.instance.pendingQuestion, QuestionsHubService.instance.pendingQuestionCount, QuestionsHubService.instance.questionPanelOpen, QuestionsHubService.instance.questionQueue, QuestionSourceConfigRepository.instance.preferences, ]), builder: (BuildContext context, Widget? child) { final int count = QuestionsHubService.instance.pendingQuestionCount.value; final bool hasPending = QuestionsHubService.instance.hasPendingQuestion.value; final sources = QuestionSourceConfigRepository.instance.current; final bool spellingOnly = QuestionSourceConfigRepository.isSpellingOnlyEnabled(sources); final bool marketOnly = QuestionSourceConfigRepository.isMarketOnlyEnabled(sources); final bool showEnvelope = spellingOnly || marketOnly || (hasPending && count >= 1); if (!showEnvelope) { return const SizedBox.shrink(); } return HubToolbarIconButton( icon: Icons.mail_outline, iconColor: AppColors.accent, badgeCount: count >= 1 ? count : null, tooltip: count == 1 ? '1 question' : count > 1 ? '$count questions' : marketOnly ? 'Market questions' : 'Spelling questions', onPressed: () => QuestionsHubService.instance.openQuestionPanel(), ); }, ), actions: [ const AdminAppBarAction(), IconButton( key: const Key('question-source-settings-button'), tooltip: 'Question sources', onPressed: () => QuestionSourceSettingsSheet.show(context), icon: const Icon(Icons.settings), ), IconButton( tooltip: 'Sync profile', onPressed: () => UserProfileRepository.instance.sync(), icon: Icon(Icons.sync, color: syncIconColor), ), IconButton( tooltip: 'Sign out', onPressed: () => AuthService.instance.signOut(), icon: const Icon(Icons.logout), ), const SizedBox(width: 4), ], ), 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, QuestionSourceConfigRepository.instance.preferences, ]), builder: (BuildContext context, Widget? child) { final QuestionsHubService hub = QuestionsHubService.instance; final bool panelOpen = hub.questionPanelOpen.value; final IncomingQuestion? question = hub.currentQuestion; final sources = QuestionSourceConfigRepository.instance.current; final bool marketEnabled = QuestionSourceConfigRepository.isMarketHistoryEnabled( sources, ); final bool spellingEnabled = QuestionSourceConfigRepository.isSpellingEnabled(sources); final bool spellingFullyWeighted = QuestionSourceConfigRepository.isSpellingFullyWeighted( sources, ); final bool showSpellingPlaceholder = panelOpen && spellingEnabled && (spellingFullyWeighted || !marketEnabled) && (question == null || !question.isSpelling); final bool showMarketPlaceholder = panelOpen && marketEnabled && !spellingEnabled && (question == null || !question.isMarketHistory); final bool showQuestionPanel = panelOpen && question != null && !showSpellingPlaceholder && !showMarketPlaceholder; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (panelOpen) _QuestionSourceConfigTopBar(sources: sources), if (showQuestionPanel) Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(8, 4, 8, 8), child: FocusScope( autofocus: true, 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: QuestionPresentationRouter( question: question, busy: hub.questionActionBusy.value, onAnswer: (num answer) => hub.submitCurrentAnswer(answer: answer), onAnswerText: hub.submitCurrentSpellingAnswer, onPrepared: hub.applyPreparedQuestion, onDefer: hub.deferCurrentQuestion, ), ), ], ), ), ), ), if (showSpellingPlaceholder) 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), ), ], ), Expanded( child: SpellingQuestionPlaceholder( busy: hub.questionActionBusy.value, onRetry: hub.openQuestionPanel, ), ), ], ), ), ), if (showMarketPlaceholder) 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), ), ], ), Expanded( child: MarketQuestionPlaceholder( busy: hub.questionActionBusy.value, onRetry: hub.openQuestionPanel, ), ), ], ), ), ), if (!showQuestionPanel && !showSpellingPlaceholder && !showMarketPlaceholder) 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: Listenable.merge([ QuestionsHubService.instance.scoreResetBusy, QuestionsHubService.instance.spellingResetBusy, ]), builder: (BuildContext context, Widget? child) { final bool blocking = QuestionsHubService.instance.scoreResetBusy.value || QuestionsHubService.instance.spellingResetBusy.value; if (!blocking) { return const SizedBox.shrink(); } return Stack( key: QuestionsHubService.instance.spellingResetBusy.value ? const Key('spelling-progress-reset-blocking-overlay') : 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 _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)), ); } } class _QuestionSourceConfigTopBar extends StatelessWidget { const _QuestionSourceConfigTopBar({ required this.sources, }); final List sources; @override Widget build(BuildContext context) { final Map mix = QuestionSourceConfigRepository.effectiveMix(sources); final String mixText = mix.isEmpty ? 'No sources enabled' : mix.entries.map((MapEntry entry) { final QuestionSourcePreference source = sources.firstWhere( (QuestionSourcePreference s) => s.id == entry.key, ); return '${source.label}: ${entry.value}%'; }).join(', '); return Container( key: const Key('question-source-config-top-bar'), decoration: BoxDecoration( color: AppColors.surfaceElevated, border: Border( bottom: BorderSide( color: AppColors.accent.withValues(alpha: 0.15), width: 1, ), ), ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ const Icon( Icons.tune_outlined, color: AppColors.accent, size: 20, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text( 'Question source configuration', style: TextStyle( color: AppColors.textPrimary, fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( mixText, style: const TextStyle( color: AppColors.textSecondary, fontSize: 11, ), overflow: TextOverflow.ellipsis, ), ], ), ), TextButton.icon( key: const Key('question-source-config-top-bar-button'), onPressed: () => QuestionSourceSettingsSheet.show(context), icon: const Icon( Icons.settings_outlined, size: 14, color: AppColors.accent, ), label: const Text( 'Configure', style: TextStyle( color: AppColors.accent, fontSize: 12, fontWeight: FontWeight.bold, ), ), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, backgroundColor: AppColors.accent.withValues(alpha: 0.08), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), side: BorderSide( color: AppColors.accent.withValues(alpha: 0.2), ), ), ), ), ], ), ); } }