Files
ReelShort/lib/pages/mine/mine_view.dart
T

456 lines
13 KiB
Dart

// 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<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
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<AuthController>();
_favorites = context.read<FavoriteController>();
_wasSignedIn = _auth.isSignedIn;
_auth.addListener(_onAuthChanged);
logic = MineBinding().create(
history: context.read<WatchHistoryRepository>(),
);
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) unawaited(_favorites.load());
});
}
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());
});
}
void _onAuthChanged() {
final signedIn = _auth.isSignedIn;
if (signedIn == _wasSignedIn) return;
_wasSignedIn = signedIn;
logic.resetRecent();
_favorites.clear();
unawaited(_reloadForIdentity());
}
Future<void> _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.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<SystemUiOverlayStyle>(
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<AuthController>();
final favorites = context.watch<FavoriteController>();
final wallet = context.watch<WalletController>();
final continueWatching = logic.recent.take(10).toList(growable: false);
final followingCount =
favorites.error == null && !(favorites.loading && favorites.total == 0)
? favorites.total
: null;
final watchRecordCount =
logic.recentError == null &&
!(logic.recentLoading && logic.watchRecordTotal == null)
? 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:
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));
}
void _openHistoryTab() {
final callback = widget.callbacks.onOpenHistoryTab;
if (callback != null) {
callback();
return;
}
unawaited(_openHistoryPage());
}
Future<void> _openHistoryPage() async {
await _push(const WatchRecordsPage());
if (mounted) unawaited(logic.refreshRecent());
}
Future<void> _push(Widget page) {
return Navigator.of(
context,
).push<void>(MaterialPageRoute(builder: (_) => page));
}
void _pushPlaceholder() {
unawaited(_push(const _MinePlaceholderPage()));
}
Future<void> _openProtected(Widget page) async {
if (!context.read<AuthController>().isSignedIn) {
await ensureSignedIn(context);
return;
}
if (mounted) await _push(page);
}
void _openVip() {
final wallet = context.read<WalletController>();
_openProtected(VipPage(onPurchased: () => unawaited(wallet.load())));
}
Future<void> _openLanguage() async {
final selected = await showModalBottomSheet<Locale>(
context: context,
backgroundColor: AppColors.transparent,
showDragHandle: false,
builder: (_) =>
_LanguageSheet(selected: context.read<LocaleController>().locale),
);
if (selected == null || !mounted) return;
await context.read<LocaleController>().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,
),
),
),
);
}
}