// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.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/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 'mine_binding.dart'; import 'mine_logic.dart'; import 'mine_types.dart'; import 'widgets/mine_header.dart'; import 'widgets/mine_menu.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 l10n = AppLocalizations.of(context)!; final content = ColoredBox( key: const Key('mine-page-surface'), color: AppColors.appBackground, child: SafeArea( bottom: false, child: Column( children: [ _topBar(l10n), Expanded(child: _body(context, l10n)), if (!embedded) _bottomNav(), ], ), ), ); return Theme( data: AppTheme.dark(), child: embedded ? content : Scaffold(backgroundColor: AppColors.appBackground, body: content), ); } Widget _topBar(AppLocalizations l10n) { return SizedBox( height: AppDimens.contentHeaderHeight, child: Padding( padding: const EdgeInsets.symmetric( horizontal: AppDimens.pageHorizontal, ), child: Align( alignment: Alignment.centerLeft, child: Text( l10n.myTab, style: const TextStyle( fontFamily: 'Outfit', color: AppColors.textPrimary, fontSize: AppDimens.minePageTitle, fontWeight: FontWeight.w700, ), ), ), ), ); } Widget _body(BuildContext context, AppLocalizations l10n) { final locale = context.watch(); return ListView( key: const Key('mine-page-scroll'), padding: const EdgeInsets.fromLTRB( AppDimens.pageHorizontal, 0, AppDimens.pageHorizontal, AppDimens.spaceXl, ), children: [ MineHeader(onLogin: _openLogin, onEdit: _openEditActions), MineVipBanner(onOpen: () => _openDestination(MineDestination.vip)), const SizedBox(height: AppDimens.spaceMd), MineMenuList( onOpen: _openDestination, onLanguage: _openLanguage, languageLabel: _languageLabel(l10n, locale.locale), ), ], ); } 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.works: _pushPlaceholder(MinePlaceholderKind.works); case MineDestination.creatorCenter: _pushPlaceholder(MinePlaceholderKind.creator); case MineDestination.notices: _push(const NoticesPage()); case MineDestination.settings: _push(const SettingsPage()); case MineDestination.help: _pushPlaceholder(MinePlaceholderKind.help); } } void _openLogin() { unawaited(openAuthPage(context)); } Future _openEditActions() async { final selected = await showModalBottomSheet( context: context, backgroundColor: AppColors.transparent, showDragHandle: false, builder: (sheetContext) => Material( color: AppColors.mineCard, borderRadius: const BorderRadius.vertical( top: Radius.circular(AppDimens.mineCardRadius), ), clipBehavior: Clip.antiAlias, child: SafeArea( top: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( key: const Key('mine-edit-profile'), title: Text(AppLocalizations.of(sheetContext)!.accountProfile), onTap: () => Navigator.pop(sheetContext, 'profile'), ), ListTile( key: const Key('mine-edit-works'), title: Text(AppLocalizations.of(sheetContext)!.myWorks), onTap: () => Navigator.pop(sheetContext, 'works'), ), ListTile( key: const Key('mine-edit-creator'), title: Text(AppLocalizations.of(sheetContext)!.creatorCenter), onTap: () => Navigator.pop(sheetContext, 'creator'), ), ListTile( key: const Key('mine-edit-notices'), title: Text(AppLocalizations.of(sheetContext)!.notices), onTap: () => Navigator.pop(sheetContext, 'notices'), ), ], ), ), ), ); if (!mounted || selected == null) return; switch (selected) { case 'profile': _push(const AccountProfilePage()); case 'works': _pushPlaceholder(MinePlaceholderKind.works); case 'creator': _pushPlaceholder(MinePlaceholderKind.creator); case 'notices': _push(const NoticesPage()); } } void _push(Widget page) { Navigator.of(context).push(MaterialPageRoute(builder: (_) => page)); } void _pushPlaceholder(MinePlaceholderKind kind) { _push(_MinePlaceholderPage(kind: kind)); } 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()))); } String _languageLabel(AppLocalizations l10n, Locale? locale) { return switch (locale?.languageCode) { 'zh' => l10n.simplifiedChinese, 'en' => l10n.english, 'id' => l10n.bahasaIndonesia, _ => l10n.language, }; } 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), ), ], ), ), ), ); } } enum MinePlaceholderKind { works, creator, help } class _MinePlaceholderPage extends StatelessWidget { const _MinePlaceholderPage({required this.kind}); final MinePlaceholderKind kind; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final title = switch (kind) { MinePlaceholderKind.works => l10n.myWorks, MinePlaceholderKind.creator => l10n.creatorCenter, MinePlaceholderKind.help => 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, ), ), ), ); } }