cyberhybridhub/lib/screens/home_screen.dart

353 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import '../models/app_user.dart';
import '../models/incoming_question.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/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 Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
centerTitle: true,
title: ListenableBuilder(
listenable: Listenable.merge(<Listenable>[
QuestionsHubService.instance.hasPendingQuestion,
QuestionsHubService.instance.pendingQuestion,
QuestionsHubService.instance.pendingQuestionCount,
]),
builder: (BuildContext context, Widget? child) {
final int count =
QuestionsHubService.instance.pendingQuestionCount.value;
final bool hasPending =
QuestionsHubService.instance.hasPendingQuestion.value;
if (!hasPending || count < 1) {
return const SizedBox.shrink();
}
final int displayCount = count;
return _QuestionEnvelopeButton(
count: displayCount,
onPressed: () =>
QuestionsHubService.instance.openQuestionPanel(),
);
},
),
actions: <Widget>[
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: <Color>[AppColors.background, AppColors.surface],
),
),
child: SafeArea(
child: ListenableBuilder(
listenable: Listenable.merge(<Listenable>[
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: <Widget>[
if (showQuestionPanel)
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Row(
children: <Widget>[
const Spacer(),
IconButton(
onPressed: hub.closeQuestionPanel,
tooltip: 'Close',
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 4),
Expanded(
child: SwipeQuestionTile(
key: ValueKey<String>(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: <Widget>[
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: <Widget>[
if (photoUrl != null)
CircleAvatar(
radius: 36,
backgroundImage: NetworkImage(photoUrl),
)
else
const CircleAvatar(
radius: 36,
child: Icon(Icons.person, size: 36),
),
const SizedBox(height: 16),
Text(
'Welcome, $displayName',
style: Theme.of(context)
.textTheme
.headlineMedium,
textAlign: TextAlign.center,
),
if (email != null) ...<Widget>[
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: <Widget>[
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)
...<Widget>[
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',
),
),
],
],
),
),
),
],
);
},
),
),
),
);
}
}
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: <Widget>[
IconButton(
onPressed: onPressed,
tooltip: count > 1 ? '$count questions' : 'New question',
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 >= 2)
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,
),
),
),
),
],
);
}
}
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)),
);
}
}