feat: 首页,收藏页的UI,字体 组件封装
This commit is contained in:
1 parent
f233f1c642
commit
7f329aef44
32 files changed
+1110
-41
No files matched your search
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../tools/widgets/app_bg_page.dart';
|
||||
|
||||
class CollectionPage extends StatefulWidget {
|
||||
const CollectionPage({super.key});
|
||||
|
||||
@@ -10,9 +12,407 @@ class CollectionPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RootPageState extends State<CollectionPage> {
|
||||
static const List<_DramaItem> _favoriteItems = [
|
||||
_DramaItem(
|
||||
title: 'An Uninvited Guest',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/09/26/66f4d0ce67383.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Vanished Without a Word',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/06/11/6667f17c9df69.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Dawn of Stardom',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/07/19/669a15b8a2e28.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Marry Me Twice',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/04/30/6630c89b746a8.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Not Your Bride',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/05/22/664daf9d042d4.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: "Husband's Hidden Crown",
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/08/12/66b9dfd8182cc.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'In the City of Love',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/07/24/66a0b88fda163.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: "Mommy's Genius Squad",
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/05/17/664716f187b2b.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Burned by Love',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/07/05/66876b720daf4.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
];
|
||||
|
||||
static const List<_DramaItem> _historyItems = [
|
||||
_DramaItem(
|
||||
title: 'Shadows of Vengeance',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/09/10/66e01c7ab24ab.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: "CEO's Naughty Wife Is Hidden Rich",
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/08/02/66accc37da3a5.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Her Veiled Protector',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/06/28/667e7312bc731.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Love Deep As The Ocean',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/06/14/666c06fb54983.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Not Your Bride',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/05/22/664daf9d042d4.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Shadows of Vengeance',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/09/10/66e01c7ab24ab.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Shadows of Vengeance',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/09/10/66e01c7ab24ab.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
];
|
||||
|
||||
bool _showHistory = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold();
|
||||
return AppBgPage(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
child: _showHistory
|
||||
? _buildHistoryList()
|
||||
: _buildFavoritesGrid(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(30, 18, 26, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildTab(
|
||||
'Favorites',
|
||||
active: !_showHistory,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_showHistory = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildTab(
|
||||
'History',
|
||||
active: _showHistory,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_showHistory = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
const Icon(Icons.edit_outlined, color: Colors.white, size: 22),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab(
|
||||
String title, {
|
||||
required bool active,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: active
|
||||
? Colors.white
|
||||
: Colors.white.withValues(alpha: 0.42),
|
||||
fontSize: 16,
|
||||
fontFamily: 'Montserrat-Bold',
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
width: active ? 54 : 0,
|
||||
height: 2,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFavoritesGrid() {
|
||||
return GridView.builder(
|
||||
key: const ValueKey('favorites'),
|
||||
padding: const EdgeInsets.fromLTRB(30, 12, 14, 96),
|
||||
itemCount: _favoriteItems.length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 14,
|
||||
childAspectRatio: 0.55,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return _FavoriteCard(item: _favoriteItems[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHistoryList() {
|
||||
return ListView.separated(
|
||||
key: const ValueKey('history'),
|
||||
padding: const EdgeInsets.fromLTRB(30, 8, 20, 96),
|
||||
itemCount: _historyItems.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 13),
|
||||
itemBuilder: (context, index) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Icon(
|
||||
Icons.bookmark_border,
|
||||
color: Colors.white.withValues(alpha: 0.28),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(child: _HistoryItem(item: _historyItems[index])),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FavoriteCard extends StatelessWidget {
|
||||
const _FavoriteCard({required this.item});
|
||||
|
||||
final _DramaItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: Image.network(
|
||||
item.imageUrl,
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxWidth * 1.33,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxWidth * 1.33,
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
item.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
height: 1.18,
|
||||
fontFamily: 'Montserrat-Bold',
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
item.episodeText,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.48),
|
||||
fontSize: 11,
|
||||
fontFamily: 'Montserrat-Medium',
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryItem extends StatelessWidget {
|
||||
const _HistoryItem({required this.item});
|
||||
|
||||
final _DramaItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 68,
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: Image.network(
|
||||
item.imageUrl,
|
||||
width: 51,
|
||||
height: 68,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 51,
|
||||
height: 68,
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
height: 1.22,
|
||||
fontFamily: 'Montserrat-Bold',
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'EP.${item.currentEpisode}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
TextSpan(text: ' /EP.${item.totalEpisode}'),
|
||||
],
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.45),
|
||||
fontSize: 12,
|
||||
fontFamily: 'Montserrat-Medium',
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DramaItem {
|
||||
const _DramaItem({
|
||||
required this.title,
|
||||
required this.imageUrl,
|
||||
required this.currentEpisode,
|
||||
required this.totalEpisode,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String imageUrl;
|
||||
final int currentEpisode;
|
||||
final int totalEpisode;
|
||||
|
||||
String get episodeText => 'EP.$currentEpisode/EP.$totalEpisode';
|
||||
}
|
||||
Reference in new issue
Block a user