281 lines
7.6 KiB
Dart
281 lines
7.6 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../core/data/models/drama.dart';
|
|
import '../../core/data/models/watch_progress.dart';
|
|
import '../../core/data/repositories/watch_history_repository.dart';
|
|
import '../../core/data/services/auth_controller.dart';
|
|
import '../../core/data/services/catalog_controller.dart';
|
|
import '../../core/data/services/download_controller.dart';
|
|
import '../../core/data/services/wallet_controller.dart';
|
|
import '../../core/theme/app_colors.dart';
|
|
import '../../core/theme/app_theme.dart';
|
|
import '../../core/widgets/app_bottom_nav.dart';
|
|
import '../../core/widgets/auth_gate.dart';
|
|
import '../../l10n/app_localizations.dart';
|
|
import '../downloads/downloads_page.dart';
|
|
import '../favorites/favorites_page.dart';
|
|
import '../player/player_view.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/continue_watching_card.dart';
|
|
import 'widgets/mine_header.dart';
|
|
import 'widgets/mine_menu.dart';
|
|
|
|
/// 极简「我的」页:续看 + 追剧 / 历史 / 下载 / 钱包 / 会员 / 设置。
|
|
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 l10n = AppLocalizations.of(context)!;
|
|
final content = SafeArea(
|
|
bottom: !embedded,
|
|
child: Column(
|
|
children: [
|
|
_topBar(l10n),
|
|
Expanded(child: _body(context, l10n)),
|
|
if (!embedded) _bottomNav(l10n),
|
|
],
|
|
),
|
|
);
|
|
return Theme(
|
|
data: AppTheme.dark(),
|
|
child: embedded
|
|
? content
|
|
: Scaffold(backgroundColor: AppColors.ink, body: content),
|
|
);
|
|
}
|
|
|
|
Widget _topBar(AppLocalizations l10n) {
|
|
return SizedBox(
|
|
height: 46,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 24,
|
|
height: 24,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.blue,
|
|
borderRadius: BorderRadius.circular(7),
|
|
),
|
|
child: const Icon(
|
|
Icons.play_arrow_rounded,
|
|
color: AppColors.onPrimary,
|
|
size: 16,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
l10n.myTab,
|
|
style: const TextStyle(
|
|
fontFamily: 'Outfit',
|
|
color: AppColors.onSurface,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _body(BuildContext context, AppLocalizations l10n) {
|
|
final auth = context.watch<AuthController>();
|
|
final wallet = context.watch<WalletController>();
|
|
final record = _continueRecord;
|
|
final drama = _dramaFor(record);
|
|
return ListView(
|
|
padding: const EdgeInsets.only(bottom: 18),
|
|
children: [
|
|
const MineHeader(),
|
|
if (drama != null && record != null)
|
|
ContinueWatchingCard(
|
|
drama: drama,
|
|
record: record,
|
|
onResume: () => _resume(drama),
|
|
),
|
|
const SizedBox(height: 14),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
|
child: MineMenu(
|
|
walletValue: _walletValue(l10n, auth, wallet),
|
|
onOpen: _openDestination,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
String _walletValue(
|
|
AppLocalizations l10n,
|
|
AuthController auth,
|
|
WalletController wallet,
|
|
) {
|
|
if (!auth.isSignedIn) return l10n.logInHint;
|
|
if (wallet.snapshot == null) return l10n.coins;
|
|
return l10n.walletBalanceShort(wallet.balance);
|
|
}
|
|
|
|
WatchProgress? get _continueRecord {
|
|
for (final record in logic.recent) {
|
|
if (!record.finished && record.hasPosition) return record;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Drama? _dramaFor(WatchProgress? record) {
|
|
if (record == null) return null;
|
|
for (final drama in context.read<CatalogController>().dramas) {
|
|
if (drama.id == record.dramaId) return drama;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void _resume(Drama drama) {
|
|
final callback = widget.callbacks.onDramaSelected;
|
|
if (callback != null) {
|
|
callback(drama);
|
|
return;
|
|
}
|
|
Navigator.of(context).push<void>(
|
|
MaterialPageRoute(
|
|
builder: (_) => PlayerPage(
|
|
drama: drama,
|
|
initialIndex: 0,
|
|
wallet: context.read<WalletController>(),
|
|
downloads: context.read<DownloadController>(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _openDestination(MineDestination destination) {
|
|
switch (destination) {
|
|
case MineDestination.watchlist:
|
|
_push(const FavoritesPage());
|
|
case MineDestination.history:
|
|
_push(const WatchRecordsPage());
|
|
case MineDestination.downloads:
|
|
_push(const DownloadsPage());
|
|
case MineDestination.wallet:
|
|
_openProtected(const WalletPage());
|
|
case MineDestination.vip:
|
|
_openVip();
|
|
case MineDestination.settings:
|
|
_push(const SettingsPage());
|
|
}
|
|
}
|
|
|
|
void _push(Widget page) {
|
|
Navigator.of(context).push<void>(MaterialPageRoute(builder: (_) => page));
|
|
}
|
|
|
|
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())));
|
|
}
|
|
|
|
Widget _bottomNav(AppLocalizations l10n) {
|
|
return AppBottomNav(
|
|
items: [
|
|
AppBottomNavItem(
|
|
tabKey: const Key('mine-nav-home'),
|
|
icon: Icons.play_circle_outline_rounded,
|
|
selectedIcon: Icons.play_circle_fill_rounded,
|
|
label: l10n.homeTab,
|
|
onTap: () => _act(MyPageAction.home),
|
|
),
|
|
AppBottomNavItem(
|
|
tabKey: const Key('mine-nav-discover'),
|
|
icon: Icons.explore_outlined,
|
|
selectedIcon: Icons.explore_rounded,
|
|
label: l10n.discoverTab,
|
|
onTap: () => _act(MyPageAction.discover),
|
|
),
|
|
AppBottomNavItem(
|
|
tabKey: const Key('mine-nav-current'),
|
|
icon: Icons.video_library_outlined,
|
|
selectedIcon: Icons.video_library_rounded,
|
|
label: l10n.myTab,
|
|
selected: true,
|
|
onTap: () {},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _act(MyPageAction action) {
|
|
logic.act(
|
|
context,
|
|
action,
|
|
embedded: embedded,
|
|
onAction: widget.callbacks.onAction,
|
|
);
|
|
}
|
|
}
|