feat(mine): 重排我的页并新增迷你播放器
This commit is contained in:
1 parent
5aa8170951
commit
cbc6d80cd4
21 files changed
+860
-531
No files matched your search
@@ -39,6 +39,9 @@ abstract final class AppColors {
|
||||
/// 金币与奖励。
|
||||
static const gold = Color(0xFFF3B34C);
|
||||
static const goldDeep = Color(0xFFD9A23A);
|
||||
static const vipGoldStart = Color(0xFFF5D9A0);
|
||||
static const vipGoldEnd = Color(0xFFE8B65A);
|
||||
static const vipBadgeSurface = Color(0x1F3D2408);
|
||||
static const bronze = Color(0xFFCD7F32);
|
||||
static const bronzeDeep = Color(0xFFA9713A);
|
||||
|
||||
@@ -69,6 +72,10 @@ abstract final class AppColors {
|
||||
static const contentChip = Color(0xFF2C2C2E);
|
||||
static const contentBorder = Color(0x1AFFFFFF);
|
||||
|
||||
/// 「我的」页卡片与描边。
|
||||
static const mineCard = Color(0xFF16161A);
|
||||
static const mineOutline = Color(0xFF4A4A4A);
|
||||
|
||||
/// 次级文字与列表 chevron。
|
||||
static const textPrimary = Color(0xFFFFFFFF);
|
||||
static const textSecondary = Color(0xFF8E8E93);
|
||||
|
||||
@@ -90,7 +90,16 @@ abstract final class AppDimens {
|
||||
|
||||
/// 「我的」页中性视觉。
|
||||
static const double mineCardRadius = 16;
|
||||
static const double mineRowHeight = 52;
|
||||
static const double mineRowHeight = 56;
|
||||
static const double minePageTitle = 21;
|
||||
static const double mineSectionTitle = 15;
|
||||
static const double mineAvatar = 56;
|
||||
static const double mineUserName = 20;
|
||||
static const double mineUid = 13;
|
||||
static const double mineMenuIcon = 22;
|
||||
static const double mineMiniWidth = 96;
|
||||
static const double mineMiniHeight = 132;
|
||||
static const double mineMiniRadius = 8;
|
||||
static const double mineMiniTitle = 10;
|
||||
static const double mineMiniClose = 22;
|
||||
}
|
||||
+8
-2
@@ -115,7 +115,6 @@
|
||||
"emptyHint": "New content will appear here.",
|
||||
"demoCatalogNotice": "Three demo dramas share the same local video.",
|
||||
"myTab": "My",
|
||||
"localViewer": "Local Viewer",
|
||||
"localDemoProfile": "Local demo profile",
|
||||
"continueWatching": "Continue Watching",
|
||||
"episodeRemaining": "Episode {episode} · {minutes} min left",
|
||||
@@ -153,7 +152,6 @@
|
||||
},
|
||||
"resume": "Resume",
|
||||
"history": "History",
|
||||
"watchlist": "Favorites",
|
||||
"likedTab": "Liked",
|
||||
"all": "All",
|
||||
"inProgress": "In Progress",
|
||||
@@ -733,6 +731,14 @@
|
||||
"modernFilter": "Modern",
|
||||
"edit": "Edit",
|
||||
"done": "Done",
|
||||
"visitor": "Visitor",
|
||||
"copyUid": "Copy UID",
|
||||
"uidCopied": "UID copied",
|
||||
"offlineDownloads": "Offline downloads",
|
||||
"watchPreferences": "Watch preferences",
|
||||
"contactSupport": "Contact support",
|
||||
"vipBrandName": "Netshort VIP",
|
||||
"mineVipSubtitle": "Enjoy exclusive benefits",
|
||||
"favoriteEpisodeProgress": "Episode {episode} / {total} total",
|
||||
"@favoriteEpisodeProgress": {
|
||||
"placeholders": {
|
||||
|
||||
+8
-2
@@ -94,7 +94,6 @@
|
||||
"emptyHint": "Konten baru akan muncul di sini.",
|
||||
"demoCatalogNotice": "Tiga drama demo memakai video lokal yang sama.",
|
||||
"myTab": "Saya",
|
||||
"localViewer": "Penonton Lokal",
|
||||
"localDemoProfile": "Profil demo lokal",
|
||||
"continueWatching": "Lanjut Tonton",
|
||||
"episodeRemaining": "Episode {episode} · {minutes} menit tersisa",
|
||||
@@ -132,7 +131,6 @@
|
||||
},
|
||||
"resume": "Lanjutkan",
|
||||
"history": "Riwayat",
|
||||
"watchlist": "Daftar Tonton",
|
||||
"likedTab": "Disukai",
|
||||
"all": "Semua",
|
||||
"inProgress": "Sedang Berjalan",
|
||||
@@ -690,6 +688,14 @@
|
||||
"modernFilter": "Modern",
|
||||
"edit": "Edit",
|
||||
"done": "Selesai",
|
||||
"visitor": "Pengunjung",
|
||||
"copyUid": "Salin UID",
|
||||
"uidCopied": "UID disalin",
|
||||
"offlineDownloads": "Unduhan offline",
|
||||
"watchPreferences": "Preferensi tontonan",
|
||||
"contactSupport": "Hubungi dukungan",
|
||||
"vipBrandName": "Netshort VIP",
|
||||
"mineVipSubtitle": "Nikmati manfaat eksklusif",
|
||||
"favoriteEpisodeProgress": "Episode {episode} / total {total}",
|
||||
"@favoriteEpisodeProgress": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -568,12 +568,6 @@ abstract class AppLocalizations {
|
||||
/// **'My'**
|
||||
String get myTab;
|
||||
|
||||
/// No description provided for @localViewer.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Local Viewer'**
|
||||
String get localViewer;
|
||||
|
||||
/// No description provided for @localDemoProfile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -628,12 +622,6 @@ abstract class AppLocalizations {
|
||||
/// **'History'**
|
||||
String get history;
|
||||
|
||||
/// No description provided for @watchlist.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Favorites'**
|
||||
String get watchlist;
|
||||
|
||||
/// No description provided for @likedTab.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -3208,6 +3196,54 @@ abstract class AppLocalizations {
|
||||
/// **'Done'**
|
||||
String get done;
|
||||
|
||||
/// No description provided for @visitor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Visitor'**
|
||||
String get visitor;
|
||||
|
||||
/// No description provided for @copyUid.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy UID'**
|
||||
String get copyUid;
|
||||
|
||||
/// No description provided for @uidCopied.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'UID copied'**
|
||||
String get uidCopied;
|
||||
|
||||
/// No description provided for @offlineDownloads.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Offline downloads'**
|
||||
String get offlineDownloads;
|
||||
|
||||
/// No description provided for @watchPreferences.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Watch preferences'**
|
||||
String get watchPreferences;
|
||||
|
||||
/// No description provided for @contactSupport.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact support'**
|
||||
String get contactSupport;
|
||||
|
||||
/// No description provided for @vipBrandName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Netshort VIP'**
|
||||
String get vipBrandName;
|
||||
|
||||
/// No description provided for @mineVipSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enjoy exclusive benefits'**
|
||||
String get mineVipSubtitle;
|
||||
|
||||
/// No description provided for @favoriteEpisodeProgress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -261,9 +261,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get myTab => 'My';
|
||||
|
||||
@override
|
||||
String get localViewer => 'Local Viewer';
|
||||
|
||||
@override
|
||||
String get localDemoProfile => 'Local demo profile';
|
||||
|
||||
@@ -301,9 +298,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get history => 'History';
|
||||
|
||||
@override
|
||||
String get watchlist => 'Favorites';
|
||||
|
||||
@override
|
||||
String get likedTab => 'Liked';
|
||||
|
||||
@@ -1671,6 +1665,30 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get done => 'Done';
|
||||
|
||||
@override
|
||||
String get visitor => 'Visitor';
|
||||
|
||||
@override
|
||||
String get copyUid => 'Copy UID';
|
||||
|
||||
@override
|
||||
String get uidCopied => 'UID copied';
|
||||
|
||||
@override
|
||||
String get offlineDownloads => 'Offline downloads';
|
||||
|
||||
@override
|
||||
String get watchPreferences => 'Watch preferences';
|
||||
|
||||
@override
|
||||
String get contactSupport => 'Contact support';
|
||||
|
||||
@override
|
||||
String get vipBrandName => 'Netshort VIP';
|
||||
|
||||
@override
|
||||
String get mineVipSubtitle => 'Enjoy exclusive benefits';
|
||||
|
||||
@override
|
||||
String favoriteEpisodeProgress(int episode, int total) {
|
||||
return 'Episode $episode / $total total';
|
||||
|
||||
@@ -261,9 +261,6 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get myTab => 'Saya';
|
||||
|
||||
@override
|
||||
String get localViewer => 'Penonton Lokal';
|
||||
|
||||
@override
|
||||
String get localDemoProfile => 'Profil demo lokal';
|
||||
|
||||
@@ -301,9 +298,6 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get history => 'Riwayat';
|
||||
|
||||
@override
|
||||
String get watchlist => 'Daftar Tonton';
|
||||
|
||||
@override
|
||||
String get likedTab => 'Disukai';
|
||||
|
||||
@@ -1673,6 +1667,30 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get done => 'Selesai';
|
||||
|
||||
@override
|
||||
String get visitor => 'Pengunjung';
|
||||
|
||||
@override
|
||||
String get copyUid => 'Salin UID';
|
||||
|
||||
@override
|
||||
String get uidCopied => 'UID disalin';
|
||||
|
||||
@override
|
||||
String get offlineDownloads => 'Unduhan offline';
|
||||
|
||||
@override
|
||||
String get watchPreferences => 'Preferensi tontonan';
|
||||
|
||||
@override
|
||||
String get contactSupport => 'Hubungi dukungan';
|
||||
|
||||
@override
|
||||
String get vipBrandName => 'Netshort VIP';
|
||||
|
||||
@override
|
||||
String get mineVipSubtitle => 'Nikmati manfaat eksklusif';
|
||||
|
||||
@override
|
||||
String favoriteEpisodeProgress(int episode, int total) {
|
||||
return 'Episode $episode / total $total';
|
||||
|
||||
@@ -256,9 +256,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get myTab => '我的';
|
||||
|
||||
@override
|
||||
String get localViewer => '本地观众';
|
||||
|
||||
@override
|
||||
String get localDemoProfile => '本地演示档案';
|
||||
|
||||
@@ -296,9 +293,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get history => '历史';
|
||||
|
||||
@override
|
||||
String get watchlist => '收藏';
|
||||
|
||||
@override
|
||||
String get likedTab => '喜欢';
|
||||
|
||||
@@ -1647,6 +1641,30 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get done => '完成';
|
||||
|
||||
@override
|
||||
String get visitor => '访客';
|
||||
|
||||
@override
|
||||
String get copyUid => '复制 UID';
|
||||
|
||||
@override
|
||||
String get uidCopied => 'UID 已复制';
|
||||
|
||||
@override
|
||||
String get offlineDownloads => '离线缓存';
|
||||
|
||||
@override
|
||||
String get watchPreferences => '观看偏好';
|
||||
|
||||
@override
|
||||
String get contactSupport => '联系客服';
|
||||
|
||||
@override
|
||||
String get vipBrandName => 'Netshort VIP';
|
||||
|
||||
@override
|
||||
String get mineVipSubtitle => '享受专属权益';
|
||||
|
||||
@override
|
||||
String favoriteEpisodeProgress(int episode, int total) {
|
||||
return '第 $episode 集 / 共 $total 集';
|
||||
|
||||
+8
-2
@@ -94,7 +94,6 @@
|
||||
"emptyHint": "有新内容时会显示在这里。",
|
||||
"demoCatalogNotice": "三部演示短剧共用同一条本地视频。",
|
||||
"myTab": "我的",
|
||||
"localViewer": "本地观众",
|
||||
"localDemoProfile": "本地演示档案",
|
||||
"continueWatching": "继续观看",
|
||||
"episodeRemaining": "第 {episode} 集 · 还剩 {minutes} 分钟",
|
||||
@@ -132,7 +131,6 @@
|
||||
},
|
||||
"resume": "继续播放",
|
||||
"history": "历史",
|
||||
"watchlist": "收藏",
|
||||
"likedTab": "喜欢",
|
||||
"all": "全部",
|
||||
"inProgress": "未看完",
|
||||
@@ -652,6 +650,14 @@
|
||||
"modernFilter": "现代",
|
||||
"edit": "编辑",
|
||||
"done": "完成",
|
||||
"visitor": "访客",
|
||||
"copyUid": "复制 UID",
|
||||
"uidCopied": "UID 已复制",
|
||||
"offlineDownloads": "离线缓存",
|
||||
"watchPreferences": "观看偏好",
|
||||
"contactSupport": "联系客服",
|
||||
"vipBrandName": "Netshort VIP",
|
||||
"mineVipSubtitle": "享受专属权益",
|
||||
"favoriteEpisodeProgress": "第 {episode} 集 / 共 {total} 集",
|
||||
"@favoriteEpisodeProgress": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -324,17 +324,15 @@ class _HomeTabsState extends State<_HomeTabs> {
|
||||
|
||||
Future<void> _openWallet(BuildContext context) async {
|
||||
if (!await ensureSignedIn(context) || !context.mounted) return;
|
||||
await Navigator.of(context)
|
||||
.push<void>(MaterialPageRoute(builder: (_) => const WalletPage()));
|
||||
await Navigator.of(
|
||||
context,
|
||||
).push<void>(MaterialPageRoute(builder: (_) => const WalletPage()));
|
||||
}
|
||||
|
||||
Widget _my(BuildContext context) {
|
||||
return MyPage(
|
||||
active: logic.tab == 3,
|
||||
callbacks: MyPageCallbacks(
|
||||
onAction: logic.myAction,
|
||||
onDramaSelected: (drama) => logic.openDrama(context, drama),
|
||||
),
|
||||
callbacks: MyPageCallbacks(onAction: logic.myAction),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user