// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../core/data/repositories/watch_history_repository.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/data/services/favorite_controller.dart'; import '../../core/data/services/locale_controller.dart'; import '../../core/data/services/wallet_controller.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/app_glow_panel.dart'; import '../../core/widgets/app_root_tab.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/plain_page_scaffold.dart'; import '../../l10n/app_localizations.dart'; import '../downloads/downloads_page.dart'; import '../account/account_profile_page.dart'; import '../favorites/favorites_page.dart'; import '../filter/filter_page.dart'; import '../notices/notices_page.dart'; import '../settings/settings_page.dart'; import '../store/vip_page.dart'; import '../wallet/wallet_page.dart'; import '../watch_records/watch_records_page.dart'; import 'mine_binding.dart'; import 'mine_logic.dart'; import 'mine_types.dart'; import 'widgets/mine_action_grid.dart'; import 'widgets/mine_header.dart'; import 'widgets/mine_menu.dart'; import 'widgets/mine_stats_panel.dart'; /// K28-B 我的页。 class MyPage extends StatefulWidget { const MyPage({ super.key, this.active = true, this.callbacks = const MyPageCallbacks(), }); final bool active; final MyPageCallbacks callbacks; @override State createState() => _MyPageState(); } class _MyPageState extends State { late final MineLogic logic; late final AuthController _auth; late final FavoriteController _favorites; late bool _wasSignedIn; bool _activationRefreshScheduled = false; bool get embedded => widget.callbacks.embedded; @override void initState() { super.initState(); _auth = context.read(); _favorites = context.read(); _wasSignedIn = _auth.isSignedIn; _auth.addListener(_onAuthChanged); logic = MineBinding().create( history: context.read(), ); if (widget.active) _scheduleFavoriteLoad(); } @override void didUpdateWidget(covariant MyPage oldWidget) { super.didUpdateWidget(oldWidget); if (widget.active && !oldWidget.active) { _scheduleActivationRefresh(); } } void _scheduleFavoriteLoad() { if (_activationRefreshScheduled) return; _activationRefreshScheduled = true; WidgetsBinding.instance.addPostFrameCallback((_) { _activationRefreshScheduled = false; if (!mounted) return; unawaited(_favorites.load()); _refreshWallet(onlyIfMissing: true); }); } void _scheduleActivationRefresh() { if (_activationRefreshScheduled) return; _activationRefreshScheduled = true; WidgetsBinding.instance.addPostFrameCallback((_) { _activationRefreshScheduled = false; if (!mounted || !widget.active) return; logic.syncPage(active: true, wasActive: false); unawaited(_favorites.load()); _refreshWallet(); }); } void _refreshWallet({bool onlyIfMissing = false}) { if (!_auth.isSignedIn) return; final wallet = context.read(); if (wallet.loading || (onlyIfMissing && wallet.snapshot != null)) return; unawaited(wallet.load()); } void _onAuthChanged() { final signedIn = _auth.isSignedIn; if (signedIn == _wasSignedIn) return; _wasSignedIn = signedIn; logic.resetRecent(); _favorites.clear(); unawaited(_reloadForIdentity()); } Future _reloadForIdentity() async { await Future.wait([logic.refreshRecent(), _favorites.load()]); } @override void dispose() { _auth.removeListener(_onAuthChanged); logic.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: logic, builder: (context, _) => _page(context), ); } Widget _page(BuildContext context) { final content = DecoratedBox( key: const Key('mine-page-surface'), // 与追剧页同款:纯黑底 + 顶部暗红光晕。 decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ AppColors.followingGlow, AppColors.followingGlowMid, AppColors.appBackground, ], stops: [0, 0.14, 0.3], ), ), child: SafeArea( bottom: false, child: Column( children: [ Expanded(child: _body(context)), if (!embedded) _bottomNav(), ], ), ), ); return AnnotatedRegion( value: SystemUiOverlayStyle.light, child: Theme( data: AppTheme.dark(), child: embedded ? content : Scaffold(backgroundColor: AppColors.appBackground, body: content), ), ); } Widget _body(BuildContext context) { return ListView( key: const Key('mine-page-scroll'), padding: const EdgeInsets.only(bottom: AppDimens.spaceXl), children: [ _section( MineHeader( onLogin: _openLogin, onEdit: () => _push(const AccountProfilePage()), ), ), const SizedBox(height: AppDimens.spaceSm), _section(_statsPanel()), const SizedBox(height: AppDimens.spaceMd), _section( MineVipBanner(onOpen: () => _openDestination(MineDestination.vip)), ), const SizedBox(height: AppDimens.mineSectionGap), Padding( padding: const EdgeInsets.symmetric( horizontal: AppDimens.mineGridHorizontal, ), child: MineActionGrid( primaryActions: MinePrimaryActions( onWallet: () => _openDestination(MineDestination.wallet), onFavorites: () => unawaited(_openFavoritesPage()), onDownloads: () => _openDestination(MineDestination.downloads), onHistory: () => _openDestination(MineDestination.history), ), serviceActions: MineServiceActions( onOpen: _openDestination, onLanguage: _openLanguage, ), ), ), ], ); } Widget _section(Widget child) { return Padding( padding: const EdgeInsets.symmetric(horizontal: AppDimens.pageHorizontal), child: child, ); } Widget _statsPanel() { return MineStatsSection( watchRecordCount: logic.recentError == null ? logic.watchRecordTotal : null, actions: MineStatsActions( onWallet: () => _openDestination(MineDestination.wallet), onHistory: () => _openDestination(MineDestination.history), onFavorites: () => unawaited(_openFavoritesPage()), ), ); } void _openDestination(MineDestination destination) { switch (destination) { case MineDestination.history: unawaited(_openHistoryPage()); case MineDestination.downloads: _push(const DownloadsPage()); case MineDestination.wallet: _openProtected(const WalletPage()); case MineDestination.vip: _openVip(); case MineDestination.preferences: _push(const FilterPage()); case MineDestination.notices: _push(const NoticesPage()); case MineDestination.settings: _push(const SettingsPage()); case MineDestination.help: _pushPlaceholder(); } } void _openLogin() { unawaited(openAuthPage(context)); } Future _openHistoryPage() async { await _push(const WatchRecordsPage()); if (mounted) unawaited(logic.refreshRecent()); } Future _openFavoritesPage() async { await _push(const FavoritesPage()); if (mounted) unawaited(_favorites.load()); } Future _push(Widget page) { return Navigator.of( context, ).push(MaterialPageRoute(builder: (_) => page)); } void _pushPlaceholder() { final l10n = AppLocalizations.of(context)!; unawaited( _push( PlainPageScaffold( pageKey: const Key('mine-placeholder-page'), backKey: const Key('mine-placeholder-back'), title: l10n.contactSupport, child: Center( child: Text( l10n.comingSoon, style: const TextStyle( color: AppColors.textSecondary, fontSize: AppDimens.body, ), ), ), ), ), ); } Future _openProtected(Widget page) async { if (!context.read().isSignedIn) { await ensureSignedIn(context); return; } if (mounted) await _push(page); } void _openVip() { final wallet = context.read(); _openProtected(VipPage(onPurchased: () => unawaited(wallet.load()))); } Future _openLanguage() async { final selected = await showModalBottomSheet( context: context, backgroundColor: AppColors.transparent, showDragHandle: false, builder: (_) => _LanguageSheet(selected: context.read().locale), ); if (selected == null || !mounted) return; await context.read().select(selected); } Widget _bottomNav() { return AppRootBottomNav( selected: AppRootTab.mine, onSelected: (tab) => _act(_actionOf(tab)), ); } MyPageAction _actionOf(AppRootTab tab) => switch (tab) { AppRootTab.home => MyPageAction.home, AppRootTab.recommend => MyPageAction.recommend, AppRootTab.reward => MyPageAction.reward, AppRootTab.following => MyPageAction.following, AppRootTab.mine => MyPageAction.mine, }; void _act(MyPageAction action) { logic.act( context, action, embedded: embedded, onAction: widget.callbacks.onAction, ); } } class _LanguageSheet extends StatelessWidget { const _LanguageSheet({required this.selected}); final Locale? selected; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final options = [ ( const Locale('zh'), const Key('mine-language-zh'), l10n.simplifiedChinese, ), (const Locale('en'), const Key('mine-language-en'), l10n.english), (const Locale('id'), const Key('mine-language-id'), l10n.bahasaIndonesia), ]; return AppGlowPanel( borderRadius: const BorderRadius.vertical( top: Radius.circular(AppDimens.mineCardRadius), ), border: false, child: Material( key: const Key('mine-language-sheet'), type: MaterialType.transparency, clipBehavior: Clip.antiAlias, child: SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( l10n.language, style: const TextStyle( color: AppColors.textPrimary, fontSize: 20, fontWeight: FontWeight.w700, ), ), ), IconButton( tooltip: l10n.close, onPressed: () => Navigator.pop(context), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 32, minHeight: 32, ), icon: const Icon(Icons.close_rounded, size: 20), ), ], ), for (final option in options) ListTile( key: option.$2, contentPadding: const EdgeInsets.symmetric(horizontal: 4), title: Text(option.$3), trailing: selected?.languageCode == option.$1.languageCode ? const Icon( Icons.check_rounded, color: AppColors.textPrimary, ) : null, onTap: () => Navigator.pop(context, option.$1), ), ], ), ), ), ), ); } }