import 'dart:async'; import 'package:flutter/material.dart'; import '../../../../core/auth/session_store.dart'; import '../../../divination/presentation/screens/divination_screen.dart'; import '../../../divination/presentation/screens/divination_result_screen.dart'; import '../../../divination/data/apis/divination_api.dart'; import '../../../divination/data/models/divination_params.dart'; import '../../../divination/data/models/divination_result.dart'; import '../../../settings/data/models/profile_settings.dart'; import '../../../settings/presentation/screens/settings_screen.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../shared/theme/app_color_palette.dart'; import '../../../../shared/theme/design_tokens.dart'; import '../../../../shared/widgets/bottom_nav_bar.dart'; import '../../../../shared/widgets/divination/divination_summary_card.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({ super.key, required this.account, required this.sessionStore, required this.currentLocale, required this.profileSettings, required this.historyRecords, required this.coinBalance, required this.divinationApi, required this.onLocaleChanged, required this.onProfileSettingsChanged, required this.onSaveProfile, required this.onUploadAvatar, required this.onDivinationCompleted, required this.onDeleteHistorySession, required this.onLogout, }); final String account; final SessionStore sessionStore; final Locale currentLocale; final ProfileSettingsV1 profileSettings; final List historyRecords; final int coinBalance; final DivinationApi divinationApi; final Future Function(String languageTag) onLocaleChanged; final Future Function(ProfileSettingsV1 settings) onProfileSettingsChanged; final Future Function(ProfileSettingsV1 updated) onSaveProfile; final Future Function(String filePath) onUploadAvatar; final Future Function(DivinationResultData result) onDivinationCompleted; final Future Function(String threadId) onDeleteHistorySession; final Future Function() onLogout; @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { MainTab _currentTab = MainTab.home; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _tryShowWelcomeDialog(); }); } Future _tryShowWelcomeDialog() async { final hasRead = await widget.sessionStore.hasReadWelcome(); if (hasRead || !mounted) { return; } await showDialog( context: context, barrierDismissible: false, builder: (context) { return _WelcomeDialog( onDone: () async { await widget.sessionStore.setWelcomeRead(true); }, ); }, ); } @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final historyItems = widget.historyRecords; return Scaffold( backgroundColor: colors.surfaceContainerLow, body: IndexedStack( index: _currentTab == MainTab.home ? 0 : 1, children: [ _HomeTab( historyItems: historyItems, sessionStore: widget.sessionStore, userId: widget.account, divinationApi: widget.divinationApi, onDivinationCompleted: widget.onDivinationCompleted, onDeleteHistorySession: widget.onDeleteHistorySession, allowVibration: widget.profileSettings.notification.allowVibration, ), _ProfileTab( account: widget.account, settings: widget.profileSettings, coinBalance: widget.coinBalance, onLocaleChanged: widget.onLocaleChanged, onSettingsChanged: widget.onProfileSettingsChanged, onSaveProfile: widget.onSaveProfile, onUploadAvatar: widget.onUploadAvatar, onLogout: widget.onLogout, ), ], ), bottomNavigationBar: BottomNavBar( currentTab: _currentTab, onTabChange: (tab) { setState(() => _currentTab = tab); }, onLogoTap: _onStartDivination, ), ); } void _onStartDivination() { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationScreen( sessionStore: widget.sessionStore, userId: widget.account, onCompleted: widget.onDivinationCompleted, allowVibration: widget.profileSettings.notification.allowVibration, ), ), ); } } class _HomeTab extends StatelessWidget { const _HomeTab({ required this.historyItems, required this.sessionStore, required this.userId, required this.divinationApi, required this.onDivinationCompleted, required this.onDeleteHistorySession, required this.allowVibration, }); final List historyItems; final SessionStore sessionStore; final String userId; final DivinationApi divinationApi; final Future Function(DivinationResultData result) onDivinationCompleted; final Future Function(String threadId) onDeleteHistorySession; final bool allowVibration; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).colorScheme; final palette = Theme.of(context).extension()!; return SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.only( top: AppSpacing.lg, bottom: AppSpacing.lg, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.historyTitle, style: Theme.of( context, ).textTheme.titleLarge?.copyWith(color: colors.primary), ), IconButton( onPressed: () { Toast.show( context, l10n.featurePending, type: ToastType.info, ); }, icon: Icon( Icons.notifications, color: colors.primary, size: 28, ), tooltip: l10n.notify, ), ], ), ), const SizedBox(height: AppSpacing.xl), Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), child: Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.xl), decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.lg), gradient: LinearGradient( colors: [colors.primary, palette.accentPurple], ), ), child: Column( children: [ Icon(Icons.auto_awesome, color: colors.onPrimary, size: 48), const SizedBox(height: AppSpacing.lg), Text( l10n.startJourney, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colors.onPrimary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.sm), Text( l10n.journeySubtitle, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: colors.onPrimary), ), const SizedBox(height: AppSpacing.lg), FilledButton( style: FilledButton.styleFrom( backgroundColor: colors.surface, foregroundColor: colors.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.sm), ), ), onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationScreen( sessionStore: sessionStore, userId: userId, onCompleted: onDivinationCompleted, allowVibration: allowVibration, ), ), ); }, child: Text(l10n.startNow), ), ], ), ), ), const SizedBox(height: AppSpacing.xl), Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.historyTitle, style: Theme.of(context).textTheme.titleMedium, ), if (historyItems.length > 4) TextButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationHistoryScreen( initialItems: historyItems, divinationApi: divinationApi, onDeleteHistorySession: onDeleteHistorySession, ), ), ); }, child: Text(l10n.more), ), ], ), ), const SizedBox(height: AppSpacing.md), if (historyItems.isEmpty) SizedBox( width: double.infinity, height: 200, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( l10n.noRecords, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: AppSpacing.sm), Text(l10n.noRecordsSubtitle), ], ), ) else Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: historyItems.take(4).map((item) { final threadId = item.threadId; return Padding( padding: const EdgeInsets.only( left: AppSpacing.md, right: AppSpacing.md, bottom: AppSpacing.md, ), child: threadId == null ? _HistoryCard( item: item, onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationResultScreen( data: item, divinationApi: null, enableIntroTransition: false, ), ), ); }, ) : Dismissible( key: ValueKey('home-history-$threadId'), direction: DismissDirection.endToStart, background: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.symmetric( horizontal: AppSpacing.lg, ), decoration: BoxDecoration( color: colors.errorContainer, borderRadius: BorderRadius.circular( AppRadius.md, ), ), child: Icon( Icons.delete_outline, color: colors.onErrorContainer, ), ), confirmDismiss: (_) async => true, onDismissed: (_) { unawaited(onDeleteHistorySession(threadId)); }, child: _HistoryCard( item: item, onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationResultScreen( data: item, divinationApi: divinationApi, enableIntroTransition: false, ), ), ); }, ), ), ); }).toList(), ), ], ), ), ); } } class DivinationHistoryScreen extends StatefulWidget { const DivinationHistoryScreen({ super.key, required this.initialItems, required this.divinationApi, required this.onDeleteHistorySession, }); final List initialItems; final DivinationApi divinationApi; final Future Function(String threadId) onDeleteHistorySession; @override State createState() => _DivinationHistoryScreenState(); } class _DivinationHistoryScreenState extends State { late List _items; @override void initState() { super.initState(); _items = List.from( widget.initialItems, growable: true, ); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar(title: Text(l10n.historyTitle)), backgroundColor: colors.surfaceContainerLow, body: _items.isEmpty ? Center(child: Text(l10n.noRecords)) : ListView.builder( padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), itemCount: _items.length, itemBuilder: (context, index) { final item = _items[index]; final threadId = item.threadId; return Padding( padding: const EdgeInsets.only( left: AppSpacing.md, right: AppSpacing.md, bottom: AppSpacing.md, ), child: threadId == null ? _HistoryCard( item: item, onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationResultScreen( data: item, divinationApi: null, enableIntroTransition: false, ), ), ); }, ) : Dismissible( key: ValueKey('history-$threadId'), direction: DismissDirection.endToStart, background: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.symmetric( horizontal: AppSpacing.lg, ), decoration: BoxDecoration( color: colors.errorContainer, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Icon( Icons.delete_outline, color: colors.onErrorContainer, ), ), confirmDismiss: (_) async => true, onDismissed: (_) { setState(() { _items.removeAt(index); }); unawaited(widget.onDeleteHistorySession(threadId)); }, child: _HistoryCard( item: item, onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationResultScreen( data: item, divinationApi: widget.divinationApi, enableIntroTransition: false, ), ), ); }, ), ), ); }, ), ); } } class _ProfileTab extends StatelessWidget { const _ProfileTab({ required this.account, required this.settings, required this.coinBalance, required this.onLocaleChanged, required this.onSettingsChanged, required this.onSaveProfile, required this.onUploadAvatar, required this.onLogout, }); final String account; final ProfileSettingsV1 settings; final int coinBalance; final Future Function(String languageTag) onLocaleChanged; final Future Function(ProfileSettingsV1 settings) onSettingsChanged; final Future Function(ProfileSettingsV1 updated) onSaveProfile; final Future Function(String filePath) onUploadAvatar; final Future Function() onLogout; @override Widget build(BuildContext context) { return SettingsScreen( account: account, settings: settings, coinBalance: coinBalance, onInterfaceLanguageChanged: onLocaleChanged, onSettingsChanged: onSettingsChanged, onSaveProfile: onSaveProfile, onUploadAvatar: onUploadAvatar, onLogout: onLogout, ); } } class _HistoryCard extends StatelessWidget { const _HistoryCard({required this.item, required this.onTap}); final DivinationResultData item; final VoidCallback onTap; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).colorScheme; final palette = Theme.of(context).extension()!; final categoryLabel = switch (item.params.questionType) { QuestionType.career => l10n.questionTypeCareer, QuestionType.love => l10n.questionTypeLove, QuestionType.wealth => l10n.questionTypeWealth, QuestionType.fortune => l10n.questionTypeFortune, QuestionType.dream => l10n.questionTypeDream, QuestionType.health => l10n.questionTypeHealth, QuestionType.study => l10n.questionTypeStudy, QuestionType.search => l10n.questionTypeSearch, QuestionType.other => l10n.questionTypeOther, }; final categoryStyle = switch (item.params.questionType) { QuestionType.career || QuestionType.study => ( palette.categoryCareerBg, palette.categoryCareerText, ), QuestionType.love => (palette.categoryLoveBg, palette.categoryLoveText), _ => (palette.categoryMoneyBg, palette.categoryMoneyText), }; final normalizedSignType = item.signType.trim(); final isBestSign = normalizedSignType.contains('上上'); final isGoodSign = !isBestSign && normalizedSignType.contains('中上'); final isWorstSign = normalizedSignType.contains('下下'); final signLabel = isBestSign ? l10n.signTypeShangShang : isGoodSign ? l10n.signTypeZhongShang : isWorstSign ? l10n.signTypeXiaXia : l10n.signTypeZhongXia; final signStyle = isBestSign ? (palette.historyGoldBg, palette.historyGoldText) : isGoodSign ? (colors.surfaceContainerHighest, colors.primary) : isWorstSign ? (colors.errorContainer, colors.onErrorContainer) : (palette.historyGrayBg, palette.historyGrayText); return SizedBox( width: double.infinity, child: DivinationSummaryCard( question: item.params.question, onTap: onTap, leading: Icon( Icons.auto_awesome, color: palette.historyBlueText, size: 22, ), leadingBackgroundColor: palette.historyBlueBg, tags: [ DivinationSummaryTagData( label: categoryLabel, background: categoryStyle.$1, foreground: categoryStyle.$2, ), DivinationSummaryTagData( label: item.guaName, background: palette.historyBlueBg, foreground: palette.historyBlueText, ), DivinationSummaryTagData( label: signLabel, background: signStyle.$1, foreground: signStyle.$2, ), ], ), ); } } class _WelcomeDialog extends StatefulWidget { const _WelcomeDialog({required this.onDone}); final Future Function() onDone; @override State<_WelcomeDialog> createState() => _WelcomeDialogState(); } class _WelcomeDialogState extends State<_WelcomeDialog> { final ScrollController _scrollController = ScrollController(); bool _hasScrolledToBottom = false; @override void initState() { super.initState(); _scrollController.addListener(_handleScroll); WidgetsBinding.instance.addPostFrameCallback((_) { _syncScrollState(); }); } @override void dispose() { _scrollController.removeListener(_handleScroll); _scrollController.dispose(); super.dispose(); } void _handleScroll() { _syncScrollState(); } void _syncScrollState() { if (!_scrollController.hasClients) { return; } final max = _scrollController.position.maxScrollExtent; final current = _scrollController.offset; final canReadAll = max <= AppSpacing.xs || current >= max - AppSpacing.md; if (_hasScrolledToBottom == canReadAll) { return; } setState(() { _hasScrolledToBottom = canReadAll; }); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).colorScheme; final palette = Theme.of(context).extension()!; return Dialog( insetPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.lg, vertical: AppSpacing.xl, ), child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 620), child: Padding( padding: const EdgeInsets.all(AppSpacing.xl), child: Column( children: [ Text( l10n.welcomeDialogTitle, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.lg), Expanded( child: SingleChildScrollView( controller: _scrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.welcomeParagraph1, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: AppSpacing.md), Text( l10n.welcomeParagraph2, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: AppSpacing.md), Text( l10n.welcomeParagraph3, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: AppSpacing.lg), Text( l10n.warningTitle, style: Theme.of(context).textTheme.titleMedium ?.copyWith(color: palette.warning), ), const SizedBox(height: AppSpacing.xs), Text( l10n.warningBody, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: palette.warning, ), ), ], ), ), ), const SizedBox(height: AppSpacing.md), if (!_hasScrolledToBottom) Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), child: Text( l10n.scrollHint, style: Theme.of(context).textTheme.bodySmall, ), ), SizedBox( width: double.infinity, child: FilledButton( onPressed: _hasScrolledToBottom ? () async { await widget.onDone(); if (!context.mounted) { return; } Navigator.of(context).pop(); } : null, style: FilledButton.styleFrom( backgroundColor: _hasScrolledToBottom ? colors.primary : colors.outline, foregroundColor: colors.onPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.sm), ), ), child: Padding( padding: const EdgeInsets.symmetric( vertical: AppSpacing.sm, ), child: Text( _hasScrolledToBottom ? l10n.understood : l10n.readAllFirst, ), ), ), ), ], ), ), ), ); } }