feat(app): 剧场页九宫格与极简我的页
This commit is contained in:
1 parent
dc8ca7b743
commit
35338cbfc8
46 files changed
+2353
-4358
No files matched your search
@@ -0,0 +1,114 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/data/models/drama.dart';
|
||||
import '../../../core/data/models/watch_progress.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/widgets/remote_image.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
|
||||
/// 「我的」页唯一保留的续看入口。
|
||||
class ContinueWatchingCard extends StatelessWidget {
|
||||
const ContinueWatchingCard({
|
||||
super.key,
|
||||
required this.drama,
|
||||
required this.record,
|
||||
required this.onResume,
|
||||
});
|
||||
|
||||
final Drama drama;
|
||||
final WatchProgress record;
|
||||
final VoidCallback onResume;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Container(
|
||||
key: const Key('mine-continue-watching'),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14),
|
||||
padding: const EdgeInsets.all(9),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.card,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
child: RemoteImage(drama.posterUrl, width: 50, height: 64),
|
||||
),
|
||||
const SizedBox(width: 9),
|
||||
Expanded(child: _info(l10n)),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
height: 28,
|
||||
child: FilledButton(
|
||||
key: const Key('mine-continue-play'),
|
||||
onPressed: onResume,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.blue,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 10),
|
||||
),
|
||||
child: Text(l10n.resume),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _info(AppLocalizations l10n) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.continueWatching,
|
||||
style: const TextStyle(
|
||||
color: AppColors.accent,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
drama.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_progressText(l10n),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 9),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _progressText(AppLocalizations l10n) {
|
||||
final episode = drama.episodes.where((item) => item.id == record.episodeId);
|
||||
final label = episode.isEmpty
|
||||
? l10n.episodeCount(drama.episodeCount)
|
||||
: l10n.episodeOfTotal(episode.first.number, drama.episodeCount);
|
||||
return '$label · ${_clock(record.position)} / ${_clock(record.duration)}';
|
||||
}
|
||||
}
|
||||
|
||||
String _clock(Duration value) {
|
||||
final minutes = value.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final seconds = value.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
return '$minutes:$seconds';
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../core/data/services/auth_controller.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
|
||||
/// 「我的」页顶部身份区,只保留头像、昵称和账号提示。
|
||||
class MineHeader extends StatelessWidget {
|
||||
const MineHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
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 Padding(
|
||||
key: const Key('mine-user-row'),
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 14, 6),
|
||||
child: Row(
|
||||
children: [
|
||||
_avatar(),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: _labels(name, subtitle)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _avatar() {
|
||||
return Container(
|
||||
key: const Key('mine-avatar'),
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF6A5CFF), AppColors.blue],
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person_rounded,
|
||||
color: AppColors.onSurface,
|
||||
size: 22,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _labels(String name, String subtitle) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 10),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
|
||||
/// 「我的」页六个核心入口。
|
||||
enum MineDestination { watchlist, history, downloads, wallet, vip, settings }
|
||||
|
||||
/// 极简功能清单,避免在主页堆叠内容列表。
|
||||
class MineMenu extends StatelessWidget {
|
||||
const MineMenu({super.key, required this.walletValue, required this.onOpen});
|
||||
|
||||
final String walletValue;
|
||||
final ValueChanged<MineDestination> onOpen;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Container(
|
||||
key: const Key('mine-menu'),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.card,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Column(children: [..._libraryTiles(l10n), ..._accountTiles(l10n)]),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _libraryTiles(AppLocalizations l10n) {
|
||||
return [
|
||||
_tile(
|
||||
key: const Key('mine-watchlist'),
|
||||
icon: Icons.bookmark_border_rounded,
|
||||
title: l10n.watchlist,
|
||||
onTap: () => onOpen(MineDestination.watchlist),
|
||||
),
|
||||
_tile(
|
||||
key: const Key('mine-history'),
|
||||
icon: Icons.history_rounded,
|
||||
title: l10n.watchHistory,
|
||||
onTap: () => onOpen(MineDestination.history),
|
||||
),
|
||||
_tile(
|
||||
key: const Key('mine-downloads'),
|
||||
icon: Icons.download_outlined,
|
||||
title: l10n.downloads,
|
||||
onTap: () => onOpen(MineDestination.downloads),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _accountTiles(AppLocalizations l10n) {
|
||||
return [
|
||||
_tile(
|
||||
key: const Key('mine-wallet'),
|
||||
icon: Icons.monetization_on_outlined,
|
||||
title: l10n.wallet,
|
||||
subtitle: walletValue,
|
||||
onTap: () => onOpen(MineDestination.wallet),
|
||||
),
|
||||
_tile(
|
||||
key: const Key('mine-vip'),
|
||||
icon: Icons.workspace_premium_outlined,
|
||||
title: l10n.vipMembership,
|
||||
onTap: () => onOpen(MineDestination.vip),
|
||||
),
|
||||
_tile(
|
||||
key: const Key('mine-settings'),
|
||||
icon: Icons.settings_outlined,
|
||||
title: l10n.settings,
|
||||
onTap: () => onOpen(MineDestination.settings),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _tile({
|
||||
required Key key,
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required VoidCallback onTap,
|
||||
String? subtitle,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: ListTile(
|
||||
key: key,
|
||||
dense: true,
|
||||
minTileHeight: 48,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
leading: Icon(icon, color: AppColors.blueSoft, size: 19),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(color: AppColors.onSurface, fontSize: 12),
|
||||
),
|
||||
subtitle: subtitle == null
|
||||
? null
|
||||
: Text(
|
||||
subtitle,
|
||||
style: const TextStyle(color: AppColors.muted, fontSize: 9),
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: AppColors.subtle,
|
||||
size: 18,
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user