// 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_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 '../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 'about_page.dart'; import 'mine_binding.dart'; import 'mine_logic.dart'; import 'mine_types.dart'; import 'widgets/mine_continue_watching.dart'; import 'widgets/mine_header.dart'; import 'widgets/mine_menu.dart'; import 'widgets/mine_more_services.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; bool get embedded => widget.callbacks.embedded; @override void initState() { super.initState(); logic = MineBinding().create( history: context.read(), ); } @override void didUpdateWidget(covariant MyPage oldWidget) { super.didUpdateWidget(oldWidget); logic.syncPage(active: widget.active, wasActive: oldWidget.active); } @override void dispose() { 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.mineGradientTop, AppColors.mineGradientMiddle, AppColors.mineGradientBottom, ], stops: [0, 0.4, 0.75], ), ), child: Stack( fit: StackFit.expand, children: [ const DecoratedBox( key: Key('mine-page-glow'), decoration: BoxDecoration( gradient: RadialGradient( center: Alignment(0, -0.95), radius: 0.9, colors: [AppColors.mineGradientGlow, AppColors.transparent], ), ), ), 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) { final auth = context.watch(); final favorites = context.watch(); final wallet = context.watch(); final continueWatching = logic.recent.take(10).toList(growable: false); final followingCount = auth.isSignedIn && favorites.error == null && !(favorites.loading && favorites.total == 0) ? favorites.total : null; final watchRecordCount = auth.isSignedIn ? logic.watchRecordTotal : null; final coinBalance = auth.isSignedIn && wallet.snapshot != null ? wallet.balance : null; return ListView( key: const Key('mine-page-scroll'), padding: const EdgeInsets.only(bottom: AppDimens.spaceXl), children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: AppDimens.pageHorizontal, ), child: MineHeader( onLogin: _openLogin, onEdit: () => _push(const AccountProfilePage()), ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: AppDimens.pageHorizontal, ), child: MineVipBanner( onOpen: () => _openDestination(MineDestination.vip), ), ), if (continueWatching.isNotEmpty) ...[ const SizedBox(height: AppDimens.mineSectionGap), MineContinueWatching( records: continueWatching, onOpenAll: _openHistoryTab, ), ], const SizedBox(height: AppDimens.mineSectionGap), Padding( padding: const EdgeInsets.symmetric( horizontal: AppDimens.mineGridHorizontal, ), child: MineStatsPanel( followingCount: followingCount, watchRecordCount: watchRecordCount, coinBalance: coinBalance, ), ), const SizedBox(height: AppDimens.mineSectionGap), Padding( padding: const EdgeInsets.symmetric( horizontal: AppDimens.mineGridHorizontal, ), child: MineMenuList( onOpen: _openDestination, onLanguage: _openLanguage, ), ), const SizedBox(height: AppDimens.mineSectionGap), Padding( padding: const EdgeInsets.symmetric( horizontal: AppDimens.mineGridHorizontal, ), child: MineMoreServices( onCommonQuestions: _pushPlaceholder, onAbout: () => _push(const MineAboutPage()), ), ), ], ); } void _openDestination(MineDestination destination) { switch (destination) { case MineDestination.history: _push(const WatchRecordsPage()); 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)); } void _openHistoryTab() { final callback = widget.callbacks.onOpenHistoryTab; if (callback != null) { callback(); return; } _push(const WatchRecordsPage()); } void _push(Widget page) { Navigator.of(context).push(MaterialPageRoute(builder: (_) => page)); } void _pushPlaceholder() { _push(const _MinePlaceholderPage()); } Future _openProtected(Widget page) async { if (!context.read().isSignedIn) { await ensureSignedIn(context); return; } if (mounted) _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.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 Material( key: const Key('mine-language-sheet'), color: AppColors.contentCard, borderRadius: const BorderRadius.vertical( top: Radius.circular(AppDimens.mineCardRadius), ), 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), icon: const Icon(Icons.close_rounded), ), ], ), 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), ), ], ), ), ), ); } } class _MinePlaceholderPage extends StatelessWidget { const _MinePlaceholderPage(); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final title = l10n.helpFeedback; return PlainPageScaffold( pageKey: const Key('mine-placeholder-page'), backKey: const Key('mine-placeholder-back'), title: title, child: Center( child: Text( l10n.comingSoon, style: const TextStyle( color: AppColors.textSecondary, fontSize: AppDimens.body, ), ), ), ); } }