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/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/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.onLocaleChanged, required this.onProfileSettingsChanged, required this.onUploadAvatar, required this.onDivinationCompleted, required this.onLogout, }); final String account; final SessionStore sessionStore; final Locale currentLocale; final ProfileSettingsV1 profileSettings; final List historyRecords; final int coinBalance; final Future Function(String languageTag) onLocaleChanged; final Future Function(ProfileSettingsV1 settings) onProfileSettingsChanged; final Future Function(String filePath) onUploadAvatar; final Future Function(DivinationResultData result) onDivinationCompleted; final Future Function() onLogout; @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { bool _showNotificationDot = true; @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 l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).colorScheme; final palette = Theme.of(context).extension()!; final historyItems = widget.historyRecords; return Scaffold( backgroundColor: colors.surfaceContainerLow, body: 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.helloUser( widget.account.isEmpty ? l10n.defaultUserName : widget.account, ), style: Theme.of( context, ).textTheme.titleLarge?.copyWith(color: colors.primary), ), Stack( children: [ IconButton( onPressed: () { setState(() { _showNotificationDot = false; }); _showSnack(context, l10n.featurePending); }, icon: Icon( Icons.notifications, color: colors.primary, size: 28, ), tooltip: l10n.notify, ), if (_showNotificationDot) Positioned( right: AppSpacing.sm, top: AppSpacing.sm, child: Container( width: 10, height: 10, decoration: BoxDecoration( color: palette.notificationDot, shape: BoxShape.circle, ), ), ), ], ), ], ), ), 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: _onStartDivination, 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, ), TextButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => _HistoryRecordsScreen( records: historyItems, onOpenResult: (item) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationResultScreen(data: item), ), ); }, ), ), ); }, 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.map((item) { return Padding( padding: const EdgeInsets.only( left: AppSpacing.md, right: AppSpacing.md, bottom: AppSpacing.md, ), child: _HistoryCard( item: item, onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationResultScreen(data: item), ), ); }, ), ); }).toList(), ), ], ), ), ), bottomNavigationBar: BottomNavBar( currentTab: MainTab.home, onTabChange: _onTabChange, onLogoTap: _onStartDivination, ), ); } void _onTabChange(MainTab tab) { if (tab == MainTab.profile) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => SettingsScreen( account: widget.account, settings: widget.profileSettings, coinBalance: widget.coinBalance, onInterfaceLanguageChanged: widget.onLocaleChanged, onSettingsChanged: widget.onProfileSettingsChanged, onUploadAvatar: widget.onUploadAvatar, onLogout: widget.onLogout, ), ), ); } } void _onStartDivination() { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationScreen( sessionStore: widget.sessionStore, userId: widget.account, onCompleted: widget.onDivinationCompleted, ), ), ); } void _showSnack(BuildContext context, String message) { Toast.show(context, message, type: ToastType.info); } } 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 || QuestionType.study => l10n.categoryCareer, QuestionType.love => l10n.categoryLove, _ => l10n.categoryMoney, }; 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 Material( color: colors.surface.withValues(alpha: 0), child: InkWell( borderRadius: BorderRadius.circular(AppRadius.md), onTap: onTap, child: Card( margin: EdgeInsets.zero, color: colors.surface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.params.question, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: AppSpacing.sm), Wrap( spacing: AppSpacing.sm, runSpacing: AppSpacing.sm, children: [ _Tag( label: categoryLabel, background: categoryStyle.$1, foreground: categoryStyle.$2, ), _Tag( label: item.guaName, background: palette.historyBlueBg, foreground: palette.historyBlueText, ), _Tag( label: signLabel, background: signStyle.$1, foreground: signStyle.$2, ), ], ), ], ), ), ), ), ); } } class _Tag extends StatelessWidget { const _Tag({ required this.label, required this.background, required this.foreground, }); final String label; final Color background; final Color foreground; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.sm, vertical: AppSpacing.xs, ), decoration: BoxDecoration( color: background, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text( label, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: foreground), ), ); } } class _HistoryRecordsScreen extends StatelessWidget { const _HistoryRecordsScreen({ required this.records, required this.onOpenResult, }); final List records; final ValueChanged onOpenResult; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colors.surfaceContainerLow, appBar: AppBar( title: Text(l10n.historyTitle), centerTitle: true, backgroundColor: colors.surfaceContainerLow, surfaceTintColor: colors.surfaceContainerLow, ), body: records.isEmpty ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( l10n.noRecords, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: AppSpacing.sm), Text(l10n.noRecordsSubtitle), ], ), ) : ListView.separated( padding: const EdgeInsets.all(AppSpacing.md), itemBuilder: (context, index) { final item = records[index]; return _HistoryCard( item: item, onTap: () => onOpenResult(item), ); }, separatorBuilder: (_, _) => const SizedBox(height: AppSpacing.md), itemCount: records.length, ), ); } } 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, ), ), ), ), ], ), ), ), ); } }