import 'dart:async'; import 'package:flutter/material.dart'; import '../../../../core/auth/session_store.dart'; import '../../../../data/network/api_client.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 '../../../notifications/data/repositories/notification_repository.dart'; import '../../../notifications/presentation/bloc/notification_bloc.dart'; import '../../../notifications/presentation/screens/notification_center_screen.dart'; import '../../../settings/data/apis/invite_api.dart'; import '../../../settings/data/models/profile_settings.dart'; import '../../../settings/data/repositories/invite_repository.dart'; import '../../../settings/presentation/screens/settings_screen.dart'; import '../../../../app/di/injection.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'; 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.notificationBloc, required this.notificationRepository, required this.onLocaleChanged, required this.onProfileSettingsChanged, required this.onSaveProfile, required this.onUploadAvatar, required this.onDivinationCompleted, required this.onDeleteHistorySession, required this.onLogout, required this.onDeleteAccount, }); final String account; final SessionStore sessionStore; final Locale currentLocale; final ProfileSettingsV1 profileSettings; final List historyRecords; final int coinBalance; final DivinationApi divinationApi; final NotificationBloc notificationBloc; final NotificationRepository notificationRepository; 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; final Future Function() onDeleteAccount; @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { MainTab _currentTab = MainTab.home; late final InviteRepository _inviteRepository; late final ApiClient _apiClient; @override void initState() { super.initState(); _apiClient = ApiClient( baseUrl: appDependencies.backendUrl, tokenProvider: widget.sessionStore.getToken, ); final inviteApi = InviteApi(apiClient: _apiClient); _inviteRepository = InviteRepositoryImpl(inviteApi: inviteApi); 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, notificationBloc: widget.notificationBloc, notificationRepository: widget.notificationRepository, profileSettings: widget.profileSettings, onProfileSettingsChanged: widget.onProfileSettingsChanged, ), _ProfileTab( account: widget.account, settings: widget.profileSettings, coinBalance: widget.coinBalance, inviteRepository: _inviteRepository, apiClient: _apiClient, onLocaleChanged: widget.onLocaleChanged, onSettingsChanged: widget.onProfileSettingsChanged, onSaveProfile: widget.onSaveProfile, onUploadAvatar: widget.onUploadAvatar, onLogout: widget.onLogout, onDeleteAccount: widget.onDeleteAccount, ), ], ), 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, profileSettings: widget.profileSettings, onProfileSettingsChanged: widget.onProfileSettingsChanged, ), ), ); } } 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, required this.notificationBloc, required this.notificationRepository, required this.profileSettings, required this.onProfileSettingsChanged, }); 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; final NotificationBloc notificationBloc; final NotificationRepository notificationRepository; final ProfileSettingsV1 profileSettings; final Future Function(ProfileSettingsV1 settings) onProfileSettingsChanged; @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: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => NotificationCenterScreen( repository: notificationRepository, onUnreadCountChanged: () { return notificationBloc.handleEvent( RefreshUnreadCount(), ); }, ), ), ); }, icon: ListenableBuilder( listenable: notificationBloc, builder: (context, _) { final count = notificationBloc.state.unreadCount; if (count > 0) { return Badge( label: Text(count > 99 ? '99+' : '$count'), child: Icon( Icons.notifications, color: colors.primary, size: 28, ), ); } return 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, profileSettings: profileSettings, onProfileSettingsChanged: onProfileSettingsChanged, ), ), ); }, 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.inviteRepository, required this.apiClient, required this.onLocaleChanged, required this.onSettingsChanged, required this.onSaveProfile, required this.onUploadAvatar, required this.onLogout, required this.onDeleteAccount, }); final String account; final ProfileSettingsV1 settings; final int coinBalance; final InviteRepository inviteRepository; final ApiClient apiClient; 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; final Future Function() onDeleteAccount; @override Widget build(BuildContext context) { return SettingsScreen( account: account, settings: settings, coinBalance: coinBalance, inviteRepository: inviteRepository, apiClient: apiClient, onInterfaceLanguageChanged: onLocaleChanged, onSettingsChanged: onSettingsChanged, onSaveProfile: onSaveProfile, onUploadAvatar: onUploadAvatar, onLogout: onLogout, onDeleteAccount: onDeleteAccount, ); } } 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, ), ), ), ), ], ), ), ), ); } }