feat(mine): 重排我的页并新增迷你播放器

This commit is contained in:
Zhou Weidong committed 2026-10-22 17:44:48 +08:00
1 parent 5aa8170951
commit cbc6d80cd4
21 files changed
+860 -531

No files matched your search

+2 -5
View File
@@ -2,18 +2,16 @@
import 'package:flutter/foundation.dart';
import '../../core/data/models/drama.dart';
/// 「我的」页与根导航之间的返回动作。
enum MyPageAction { home, recommend, following, mine, resume }
/// 「我的」页可进入的功能目的地。
enum MineDestination {
watchlist,
history,
downloads,
wallet,
vip,
preferences,
works,
creatorCenter,
notices,
@@ -23,10 +21,9 @@ enum MineDestination {
/// 「我的」页导航回调;没有 `onAction` 时按独立页面处理。
class MyPageCallbacks {
const MyPageCallbacks({this.onAction, this.onDramaSelected});
const MyPageCallbacks({this.onAction});
final ValueChanged<MyPageAction>? onAction;
final ValueChanged<Drama>? onDramaSelected;
bool get embedded => onAction != null;
}
+130 -43
View File
@@ -5,8 +5,11 @@ 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/locale_controller.dart';
import '../../core/data/services/wallet_controller.dart';
import '../../core/theme/app_colors.dart';
@@ -17,10 +20,11 @@ import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart';
import '../downloads/downloads_page.dart';
import '../favorites/favorites_page.dart';
import '../account/account_profile_page.dart';
import '../filter/filter_page.dart';
import '../notices/notices_page.dart';
import '../player/open_player.dart';
import '../settings/settings_page.dart';
import '../store/recharge_page.dart';
import '../store/vip_page.dart';
import '../wallet/wallet_page.dart';
import '../watch_records/watch_records_page.dart';
@@ -29,6 +33,7 @@ import 'mine_logic.dart';
import 'mine_types.dart';
import 'widgets/mine_header.dart';
import 'widgets/mine_menu.dart';
import 'widgets/mine_mini_player.dart';
/// K28-B 我的页。
class MyPage extends StatefulWidget {
@@ -47,6 +52,7 @@ class MyPage extends StatefulWidget {
class _MyPageState extends State<MyPage> {
late final MineLogic logic;
bool _miniDismissed = false;
bool get embedded => widget.callbacks.embedded;
@@ -126,53 +132,51 @@ class _MyPageState extends State<MyPage> {
}
Widget _body(BuildContext context, AppLocalizations l10n) {
final auth = context.watch<AuthController>();
final wallet = context.watch<WalletController>();
final locale = context.watch<LocaleController>();
return ListView(
key: const Key('mine-page-scroll'),
padding: const EdgeInsets.fromLTRB(
AppDimens.pageHorizontal,
0,
AppDimens.pageHorizontal,
AppDimens.spaceXl,
),
final recent = _miniRecord;
return Stack(
children: [
MineHeader(onTap: () => _openDestination(MineDestination.settings)),
const SizedBox(height: AppDimens.spaceMd),
MineShortcutGrid(onOpen: _openDestination),
const SizedBox(height: AppDimens.spaceMd),
MineWalletCard(
balance: _walletValue(l10n, auth, wallet),
onOpen: () => _openDestination(MineDestination.wallet),
onRecharge: _openRecharge,
),
const SizedBox(height: AppDimens.spaceMd),
MineVipCard(onOpen: () => _openDestination(MineDestination.vip)),
const SizedBox(height: AppDimens.spaceMd),
MineMenuList(
onOpen: _openDestination,
onLanguage: _openLanguage,
languageLabel: _languageLabel(l10n, locale.locale),
Positioned.fill(
child: ListView(
key: const Key('mine-page-scroll'),
padding: EdgeInsets.fromLTRB(
AppDimens.pageHorizontal,
0,
AppDimens.pageHorizontal,
recent == null ? AppDimens.spaceXl : 156,
),
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),
),
],
),
),
if (recent != null)
Positioned(
left: AppDimens.pageHorizontal,
bottom: AppDimens.spaceMd,
child: MineMiniPlayer(
title: _miniTitle(l10n, recent),
coverUrl: recent.dramaCover?.trim() ?? '',
episodeLabel: l10n.episodeLabel(recent.episodeNumber ?? 0),
onTap: () => unawaited(_openRecent(recent)),
onClose: () => setState(() => _miniDismissed = true),
),
),
],
);
}
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);
}
void _openDestination(MineDestination destination) {
switch (destination) {
case MineDestination.watchlist:
_push(const FavoritesPage());
case MineDestination.history:
_push(const WatchRecordsPage());
case MineDestination.downloads:
@@ -181,6 +185,8 @@ class _MyPageState extends State<MyPage> {
_openProtected(const WalletPage());
case MineDestination.vip:
_openVip();
case MineDestination.preferences:
_push(const FilterPage());
case MineDestination.works:
_pushPlaceholder(MinePlaceholderKind.works);
case MineDestination.creatorCenter:
@@ -194,6 +200,64 @@ class _MyPageState extends State<MyPage> {
}
}
void _openLogin() {
unawaited(openAuthPage(context));
}
Future<void> _openEditActions() async {
final selected = await showModalBottomSheet<String>(
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<void>(MaterialPageRoute(builder: (_) => page));
}
@@ -236,9 +300,32 @@ class _MyPageState extends State<MyPage> {
await context.read<LocaleController>().select(selected);
}
void _openRecharge() {
final wallet = context.read<WalletController>();
_openProtected(RechargePage(onPurchased: () => unawaited(wallet.load())));
WatchProgress? get _miniRecord {
if (_miniDismissed || logic.recent.isEmpty) return null;
final recent = logic.recent.first;
return recent.episodeId.isEmpty ? null : recent;
}
String _miniTitle(AppLocalizations l10n, WatchProgress recent) {
final title = recent.dramaTitle?.trim() ?? '';
return title.isEmpty ? l10n.episodeLabel(recent.episodeNumber ?? 0) : title;
}
Future<void> _openRecent(WatchProgress recent) async {
final catalog = context.read<CatalogController>();
final seed = Drama(
id: recent.dramaId,
title: recent.dramaTitle?.trim() ?? '',
synopsis: '',
posterUrl: recent.dramaCover?.trim() ?? '',
);
try {
final detail = await catalog.requireDetail(seed);
if (!mounted) return;
await playDramaFromCatalog(context, detail, episodeId: recent.episodeId);
} catch (_) {
// 最近记录失效时不投射错误页,等待下一次记录刷新。
}
}
Widget _bottomNav() {
+120 -60
View File
@@ -1,19 +1,28 @@
// 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/services/auth_controller.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/centered_toast.dart';
import '../../../core/widgets/remote_image.dart';
import '../../../l10n/app_localizations.dart';
/// 「我的」页顶部身份卡。
class MineHeader extends StatelessWidget {
const MineHeader({super.key, required this.onTap});
const double _buttonHeight = 34;
const double _buttonRadius = 20;
const double _copyIconSize = 15;
final VoidCallback onTap;
/// 「我的」页用户区。
class MineHeader extends StatelessWidget {
const MineHeader({super.key, required this.onLogin, required this.onEdit});
final VoidCallback onLogin;
final VoidCallback onEdit;
@override
Widget build(BuildContext context) {
@@ -21,41 +30,80 @@ class MineHeader extends StatelessWidget {
final auth = context.watch<AuthController>();
final user = auth.user;
final signedIn = auth.isSignedIn;
final name = signedIn && (user?.nickname.isNotEmpty ?? false)
? user!.nickname
: l10n.localViewer;
final subtitle = signedIn && (user?.uid.isNotEmpty ?? false)
? 'ID: ${user!.uid}'
: l10n.logInHint;
return Container(
final nickname = signedIn && (user?.nickname.trim().isNotEmpty ?? false)
? user!.nickname.trim()
: l10n.visitor;
final uid = signedIn ? user?.uid.trim() ?? '' : '';
return Padding(
key: const Key('mine-user-row'),
decoration: BoxDecoration(
color: AppColors.contentCard,
borderRadius: BorderRadius.circular(AppDimens.mineCardRadius),
border: Border.all(color: AppColors.contentBorder),
),
child: Material(
color: AppColors.transparent,
borderRadius: BorderRadius.circular(AppDimens.mineCardRadius),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(AppDimens.cardPadding),
child: Row(
padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceMd),
child: Row(
children: [
_avatar(user?.avatarUrl ?? ''),
const SizedBox(width: AppDimens.spaceMd),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_avatar(user?.avatarUrl ?? ''),
const SizedBox(width: AppDimens.spaceMd),
Expanded(child: _labels(name, subtitle)),
const Icon(
Icons.chevron_right_rounded,
color: AppColors.textSecondary,
size: 20,
Text(
nickname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.textPrimary,
fontSize: AppDimens.mineUserName,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: AppDimens.spaceXs),
Row(
children: [
Flexible(
child: Text(
'${l10n.accountUid}: ${uid.isEmpty ? '--' : uid}',
key: const Key('mine-uid'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: AppDimens.mineUid,
),
),
),
const SizedBox(width: AppDimens.spaceXs),
IconButton(
key: const Key('mine-uid-copy'),
tooltip: l10n.copyUid,
onPressed: uid.isEmpty
? null
: () => unawaited(
_copyUid(context, uid, l10n.uidCopied),
),
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints.tightFor(
width: AppDimens.tapTarget,
height: AppDimens.tapTarget,
),
padding: EdgeInsets.zero,
icon: const Icon(
Icons.copy_rounded,
color: AppColors.textSecondary,
size: _copyIconSize,
),
),
],
),
],
),
),
),
const SizedBox(width: AppDimens.spaceSm),
_accountButton(
key: signedIn ? const Key('mine-edit') : const Key('mine-login'),
label: signedIn ? l10n.edit : l10n.logIn,
onTap: signedIn ? onEdit : onLogin,
),
],
),
);
}
@@ -63,8 +111,8 @@ class MineHeader extends StatelessWidget {
Widget _avatar(String path) {
return Container(
key: const Key('mine-avatar'),
width: 64,
height: 64,
width: AppDimens.mineAvatar,
height: AppDimens.mineAvatar,
decoration: const BoxDecoration(
color: AppColors.contentSurface,
shape: BoxShape.circle,
@@ -74,38 +122,50 @@ class MineHeader extends StatelessWidget {
? const Icon(
Icons.person_rounded,
color: AppColors.textPrimary,
size: 30,
size: 28,
)
: RemoteImage(path, fit: BoxFit.cover),
);
}
Widget _labels(String name, String subtitle) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
Widget _accountButton({
required Key key,
required String label,
required VoidCallback onTap,
}) {
return OutlinedButton(
key: key,
onPressed: onTap,
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, _buttonHeight),
padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceMd),
foregroundColor: AppColors.textPrimary,
side: const BorderSide(color: AppColors.mineOutline),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_buttonRadius),
),
const SizedBox(height: AppDimens.spaceXs),
Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: AppDimens.caption,
),
textStyle: const TextStyle(
fontSize: AppDimens.body,
fontWeight: FontWeight.w500,
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(label),
const SizedBox(width: 2),
const Icon(Icons.chevron_right_rounded, size: 17),
],
),
);
}
Future<void> _copyUid(
BuildContext context,
String uid,
String message,
) async {
await Clipboard.setData(ClipboardData(text: uid));
if (context.mounted) showCenteredToast(context, message);
}
}
+90 -232
View File
@@ -7,180 +7,13 @@ import '../../../core/theme/app_dimens.dart';
import '../../../l10n/app_localizations.dart';
import '../mine_types.dart';
/// 我的页三宫格快捷入口。
class MineShortcutGrid extends StatelessWidget {
const MineShortcutGrid({super.key, required this.onOpen});
const double _vipRadius = 12;
const double _vipBadgeSize = 36;
const double _vipButtonHeight = 34;
final ValueChanged<MineDestination> onOpen;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Row(
children: [
Expanded(
child: _ShortcutTile(
key: const Key('mine-watchlist'),
icon: Icons.bookmark_border_rounded,
label: l10n.watchlist,
onTap: () => onOpen(MineDestination.watchlist),
),
),
const SizedBox(width: AppDimens.homeGridGap),
Expanded(
child: _ShortcutTile(
key: const Key('mine-history'),
icon: Icons.history_rounded,
label: l10n.watchHistory,
onTap: () => onOpen(MineDestination.history),
),
),
const SizedBox(width: AppDimens.homeGridGap),
Expanded(
child: _ShortcutTile(
key: const Key('mine-downloads'),
icon: Icons.download_outlined,
label: l10n.downloads,
onTap: () => onOpen(MineDestination.downloads),
),
),
],
);
}
}
class _ShortcutTile extends StatelessWidget {
const _ShortcutTile({
super.key,
required this.icon,
required this.label,
required this.onTap,
});
final IconData icon;
final String label;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.contentCard,
borderRadius: BorderRadius.circular(AppDimens.mineCardRadius),
border: Border.all(color: AppColors.contentBorder),
),
child: Material(
color: AppColors.transparent,
borderRadius: BorderRadius.circular(AppDimens.mineCardRadius),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: SizedBox(
height: 88,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: AppColors.textPrimary, size: 24),
const SizedBox(height: AppDimens.spaceSm),
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.caption,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
);
}
}
/// 金币余额卡。
class MineWalletCard extends StatelessWidget {
const MineWalletCard({
super.key,
required this.balance,
required this.onOpen,
required this.onRecharge,
});
final String balance;
final VoidCallback onOpen;
final VoidCallback onRecharge;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
key: const Key('mine-wallet'),
padding: const EdgeInsets.all(AppDimens.cardPadding),
decoration: BoxDecoration(
color: AppColors.contentCard,
borderRadius: BorderRadius.circular(AppDimens.mineCardRadius),
border: Border.all(color: AppColors.contentBorder),
),
child: Row(
children: [
const Icon(
Icons.toll_outlined,
color: AppColors.textSecondary,
size: 28,
),
const SizedBox(width: AppDimens.spaceMd),
Expanded(
child: InkWell(
onTap: onOpen,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.coinBalance,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: AppDimens.caption,
),
),
const SizedBox(height: 3),
Text(
balance,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.pageTitle,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
TextButton(
key: const Key('mine-wallet-recharge'),
onPressed: onRecharge,
style: TextButton.styleFrom(
backgroundColor: AppColors.blue,
foregroundColor: AppColors.onPrimary,
minimumSize: const Size(64, AppDimens.buttonCompact),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimens.buttonRadius),
),
),
child: Text(l10n.topUpWallet),
),
],
),
);
}
}
/// 会员权益卡。
class MineVipCard extends StatelessWidget {
const MineVipCard({super.key, required this.onOpen});
/// 金色会员横幅。
class MineVipBanner extends StatelessWidget {
const MineVipBanner({super.key, required this.onOpen});
final VoidCallback onOpen;
@@ -190,30 +23,34 @@ class MineVipCard extends StatelessWidget {
return Container(
key: const Key('mine-vip'),
decoration: BoxDecoration(
color: AppColors.contentCard,
borderRadius: BorderRadius.circular(AppDimens.mineCardRadius),
border: Border.all(color: AppColors.contentBorder),
gradient: const LinearGradient(
colors: [AppColors.vipGoldStart, AppColors.vipGoldEnd],
),
borderRadius: BorderRadius.circular(_vipRadius),
),
child: Material(
color: AppColors.transparent,
borderRadius: BorderRadius.circular(AppDimens.mineCardRadius),
borderRadius: BorderRadius.circular(_vipRadius),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onOpen,
child: Padding(
padding: const EdgeInsets.all(AppDimens.cardPadding),
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.cardPadding,
vertical: AppDimens.spaceMd,
),
child: Row(
children: [
Container(
width: AppDimens.buttonLarge,
height: AppDimens.buttonLarge,
width: _vipBadgeSize,
height: _vipBadgeSize,
decoration: const BoxDecoration(
color: AppColors.contentSurface,
color: AppColors.vipBadgeSurface,
shape: BoxShape.circle,
),
child: const Icon(
Icons.workspace_premium_rounded,
color: AppColors.textPrimary,
color: AppColors.onGold,
size: 22,
),
),
@@ -223,33 +60,44 @@ class MineVipCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.vipMembership,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.mineSectionTitle,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 3),
Text(
l10n.vipUnlockMessage,
l10n.vipBrandName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textSecondary,
color: AppColors.onGold,
fontSize: AppDimens.sectionTitle,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
l10n.mineVipSubtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onGold,
fontSize: AppDimens.caption,
),
),
],
),
),
Text(
l10n.joinVip,
style: const TextStyle(
color: AppColors.blue,
fontSize: AppDimens.body,
fontWeight: FontWeight.w700,
const SizedBox(width: AppDimens.spaceSm),
TextButton(
key: const Key('mine-vip-open'),
onPressed: onOpen,
style: TextButton.styleFrom(
minimumSize: const Size(0, _vipButtonHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.spaceMd,
),
backgroundColor: AppColors.onGold,
foregroundColor: AppColors.textPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_vipButtonHeight),
),
),
child: Text(l10n.joinVip),
),
],
),
@@ -260,7 +108,7 @@ class MineVipCard extends StatelessWidget {
}
}
/// 我的页功能菜单。
/// 「我的」页单列功能菜单。
class MineMenuList extends StatelessWidget {
const MineMenuList({
super.key,
@@ -279,7 +127,7 @@ class MineMenuList extends StatelessWidget {
return Container(
key: const Key('mine-menu'),
decoration: BoxDecoration(
color: AppColors.contentCard,
color: AppColors.mineCard,
borderRadius: BorderRadius.circular(AppDimens.mineCardRadius),
border: Border.all(color: AppColors.contentBorder),
),
@@ -290,57 +138,63 @@ class MineMenuList extends StatelessWidget {
child: Column(
children: [
_row(
key: const Key('mine-works'),
icon: Icons.video_collection_outlined,
title: l10n.myWorks,
destination: MineDestination.works,
key: const Key('mine-wallet'),
icon: Icons.account_balance_wallet_outlined,
title: l10n.wallet,
destination: MineDestination.wallet,
),
_divider(),
_row(
key: const Key('mine-creator-center'),
icon: Icons.auto_awesome_rounded,
title: l10n.creatorCenter,
destination: MineDestination.creatorCenter,
key: const Key('mine-downloads'),
icon: Icons.download_outlined,
title: l10n.offlineDownloads,
destination: MineDestination.downloads,
),
_divider(),
_row(
key: const Key('mine-notices'),
icon: Icons.notifications_none_rounded,
title: l10n.notices,
destination: MineDestination.notices,
key: const Key('mine-history'),
icon: Icons.history_rounded,
title: l10n.watchHistory,
destination: MineDestination.history,
),
_divider(),
_row(
key: const Key('mine-watch-preferences'),
icon: Icons.tune_rounded,
title: l10n.watchPreferences,
destination: MineDestination.preferences,
),
_divider(),
_row(
key: const Key('mine-help'),
icon: Icons.support_agent_rounded,
title: l10n.contactSupport,
destination: MineDestination.help,
),
_divider(),
_languageRow(l10n),
_divider(),
_row(
key: const Key('mine-settings'),
icon: Icons.settings_outlined,
title: l10n.settings,
destination: MineDestination.settings,
),
_divider(),
_languageRow(context),
_divider(),
_row(
key: const Key('mine-help'),
icon: Icons.help_outline_rounded,
title: l10n.helpFeedback,
destination: MineDestination.help,
),
],
),
),
);
}
Widget _languageRow(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
Widget _languageRow(AppLocalizations l10n) {
return ListTile(
key: const Key('mine-language'),
minTileHeight: AppDimens.mineRowHeight,
contentPadding: const EdgeInsets.symmetric(horizontal: 14),
contentPadding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceLg),
leading: const Icon(
Icons.language_rounded,
color: AppColors.textSecondary,
size: 20,
color: AppColors.textPrimary,
size: AppDimens.mineMenuIcon,
),
title: Text(
l10n.language,
@@ -360,7 +214,7 @@ class MineMenuList extends StatelessWidget {
fontSize: AppDimens.caption,
),
),
const SizedBox(width: 4),
const SizedBox(width: AppDimens.spaceXs),
const Icon(
Icons.chevron_right_rounded,
color: AppColors.textSecondary,
@@ -381,8 +235,12 @@ class MineMenuList extends StatelessWidget {
return ListTile(
key: key,
minTileHeight: AppDimens.mineRowHeight,
contentPadding: const EdgeInsets.symmetric(horizontal: 14),
leading: Icon(icon, color: AppColors.textSecondary, size: 20),
contentPadding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceLg),
leading: Icon(
icon,
color: AppColors.textPrimary,
size: AppDimens.mineMenuIcon,
),
title: Text(
title,
style: const TextStyle(
@@ -403,8 +261,8 @@ class MineMenuList extends StatelessWidget {
Widget _divider() {
return const Divider(
height: 1,
indent: 14,
endIndent: 14,
indent: AppDimens.spaceLg,
endIndent: AppDimens.spaceLg,
color: AppColors.contentSurface,
);
}
@@ -0,0 +1,124 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/remote_image.dart';
const double _infoHeight = 42;
/// 「我的」页左下角最近观看迷你播放器。
class MineMiniPlayer extends StatelessWidget {
const MineMiniPlayer({
super.key,
required this.title,
required this.coverUrl,
required this.episodeLabel,
required this.onTap,
required this.onClose,
});
final String title;
final String coverUrl;
final String episodeLabel;
final VoidCallback onTap;
final VoidCallback onClose;
@override
Widget build(BuildContext context) {
return SizedBox(
key: const Key('mine-mini-player'),
width: AppDimens.mineMiniWidth,
height: AppDimens.mineMiniHeight,
child: DecoratedBox(
decoration: BoxDecoration(
color: AppColors.mineCard,
borderRadius: BorderRadius.circular(AppDimens.mineMiniRadius),
border: Border.all(color: AppColors.contentBorder),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(AppDimens.mineMiniRadius),
child: Material(
color: AppColors.transparent,
child: InkWell(
onTap: onTap,
child: Stack(
children: [
Column(
children: [
Expanded(
child: RemoteImage(
coverUrl,
fit: BoxFit.cover,
showFallbackIcon: true,
fallbackColor: AppColors.contentSurface,
),
),
SizedBox(
height: _infoHeight,
child: ColoredBox(
color: AppColors.mineCard,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 4,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.mineMiniTitle,
fontWeight: FontWeight.w600,
),
),
Text(
episodeLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: AppDimens.mineMiniTitle,
),
),
],
),
),
),
),
],
),
Positioned(
right: 3,
top: 3,
child: IconButton(
key: const Key('mine-mini-close'),
tooltip: MaterialLocalizations.of(
context,
).closeButtonTooltip,
onPressed: onClose,
style: IconButton.styleFrom(
minimumSize: const Size.square(AppDimens.mineMiniClose),
maximumSize: const Size.square(AppDimens.mineMiniClose),
padding: EdgeInsets.zero,
backgroundColor: AppColors.controlScrim,
foregroundColor: AppColors.textPrimary,
),
iconSize: 14,
icon: const Icon(Icons.close_rounded),
),
),
],
),
),
),
),
),
);
}
}