402 lines
12 KiB
Dart
402 lines
12 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;
|
|
|
|
bool get embedded => widget.callbacks.embedded;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
logic = MineBinding().create(
|
|
history: context.read<WatchHistoryRepository>(),
|
|
);
|
|
}
|
|
|
|
@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<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 =
|
|
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<void>(MaterialPageRoute(builder: (_) => page));
|
|
}
|
|
|
|
void _pushPlaceholder() {
|
|
_push(const _MinePlaceholderPage());
|
|
}
|
|
|
|
Future<void> _openProtected(Widget page) async {
|
|
if (!context.read<AuthController>().isSignedIn) {
|
|
await ensureSignedIn(context);
|
|
return;
|
|
}
|
|
if (mounted) _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,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|