feat: 首页,收藏页的UI,字体 组件封装

This commit is contained in:
chengsiwen committed 2026-07-03 15:38:01 +08:00
1 parent f233f1c642
commit 7f329aef44
32 files changed
+1110 -41

No files matched your search

+402 -2
View File
@@ -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';
}
+27 -1
View File
@@ -1,4 +1,9 @@
import 'package:flutter/material.dart';
import 'package:novyronst/common/help/help_image.dart';
import 'package:novyronst/pages/first/widgets/home_first_view.dart';
import 'package:novyronst/pages/first/widgets/search_view.dart';
import '../../../tools/widgets/app_bg_page.dart';
class FirstPage extends StatefulWidget {
const FirstPage({super.key});
@@ -13,6 +18,27 @@ class _RootPageState extends State<FirstPage> {
@override
Widget build(BuildContext context) {
return Scaffold();
return AppBgPage(
child: Column(
children: [
HomeSearchView(),
Expanded(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: HomeFirstView(),
),
SliverPadding(
padding: const EdgeInsets.only(left: 16, right: 10, top: 16),
sliver: SliverToBoxAdapter(
child: HelpImage.asset(ConstImage.homeRank),
)
)
],
),
)
],
),
);
}
}
@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:novyronst/pages/widgets/cut_corner_clipper.dart';
import 'package:novyronst/pages/widgets/ny_progress_widget.dart';
import 'package:novyronst/pages/widgets/ny_tag_widget.dart';
import 'package:novyronst/root/lib_export.dart';
import 'package:novyronst/tools/widgets/novy_button_bg.dart';
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
import 'package:novyronst/tools/widgets/novy_img_bg.dart';
class HomeFirstView extends StatefulWidget {
const HomeFirstView({super.key});
@override
State<StatefulWidget> createState() {
return _HomeFirstViewState();
}
}
class _HomeFirstViewState extends State<HomeFirstView> {
bool _hasHistory = true;
@override
Widget build(BuildContext context) {
const double _space = 25;
return Stack(
children: [
// 151
SizedBox(
height: 151 - _space + 200 + 52 + 20,
),
HelpImage.asset(ConstImage.homeFirst, width: AppScreen.screenWidth, fit: BoxFit.fill),
Positioned(
top: 151-_space, left: 16, right: 16,
child: Column(
children: [
CutCornerClipper(
width: AppScreen.screenWidth - 32,
height: 204,
cutSize: 80,
child: _buildVideoItem(),
),
const SizedBox(height: 16),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(3, (index) {
return Container(
margin: const EdgeInsets.only(right: 8),
child: NovyImgBg(imgBg: ConstImage.cateBg,
height: 52,
child: Stack(
children:[
NovyText('ClassPp', color: ConstColor.btGreen,
fontSize: 21,
textAlign: TextAlign.center,
fontFamily: ConstFont.montBlack,
shadow: Shadow(
offset: const Offset(1, 1), // X: 1, Y: 1
blurRadius: 0, // Blur: 0
color: Colors.black, // #000000
),
),
Positioned(
bottom: 0, left: 0, right: 0,
child: Container(
height: 2,
color: Colors.black,
),
)
]
),
),
);
}),
),
),
],
)
)
],
);
}
_buildVideoItem() {
return Padding(
padding: const EdgeInsets.all(8),
child: Column(
children: [
Row(
children: [
NovyText('Current Viewing Progress',
color: Colors.black,
fontSize: 16,
fontFamily: ConstFont.montBlack,
),
const SizedBox(width: 10),
HelpImage.asset(ConstImage.homeCurrentTag, height: 10)
],
),
const SizedBox(height: 10),
Row(
children: [
Container(
color: Colors.grey.withAlpha(200),
width: 60, height: 90,
),
const SizedBox(width: 12),
SizedBox(
height: 90,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NovyText('Jdksyjs dsaoy',
color: Colors.black,
fontSize: 14,
fontFamily: ConstFont.montMedium,
),
const SizedBox(height: 4),
NovyText('Jdksyjs dsaoy dasku dsaldsa dljas',
color: Colors.grey,
fontSize: 10,
fontFamily: ConstFont.montRegular,
maxLines: 2,
),
Spacer(),
Row(
children: [
NyTagWidget(text: 'Safiray'),
const SizedBox(width: 8),
NyHotWidget(text: '12.4k'),
],
)
],
),
)
],
),
const SizedBox(height: 10),
NyProgressWidget(),
const SizedBox(height: 6),
NovyButtonBg(text: 'Keep Watching', type: 2, width: 327.w,)
],
),
);
}
}
+78
View File
@@ -0,0 +1,78 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:novyronst/root/lib_export.dart';
class HomeSearchView extends StatefulWidget {
const HomeSearchView({super.key});
@override
State<StatefulWidget> createState() {
return _SearchViewState();
}
}
class _SearchViewState extends State<HomeSearchView> {
final _searchController = TextEditingController();
@override
Widget build(BuildContext context) {
return Container(
height: AppScreen.navBarHeight,
padding: EdgeInsets.only(top: AppScreen.statusBarHeight,
left: 16, right: 16,
bottom: 5
),
child: SizedBox(
height: 36,
child: Row(
children: [
HelpImage.asset(ConstImage.homeTopLogo, width: 36),
const SizedBox(width: 16),
Expanded(
child: Stack(
children: [
HelpImage.asset(ConstImage.searchBg, height: 36, fit: BoxFit.fill),
Padding(
padding: EdgeInsets.only(left: 11),
child: Row(
children: [
Icon(Icons.search, color: Colors.white, size: 24),
Expanded(child: CupertinoTextField(
controller: _searchController,
// focusNode: widget.focusNode,
decoration: BoxDecoration(),
placeholder: 'What are...... ',
placeholderStyle: TextStyle(
fontSize: 13,
color: Colors.white.withAlpha(127),
),
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
textInputAction: TextInputAction.search,
onEditingComplete: () {
},
),),
HelpImage.asset(ConstImage.homeSearch, width: 78, height: 36, fit: BoxFit.fill)
],
),
)
],
),
)
],
),
),
);
}
}
+101
View File
@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
class CutCornerClipper extends StatelessWidget {
const CutCornerClipper({super.key,
this.width,
this.height,
this.cutCorner = CutCorner.topRight,
this.cutSize,
this.child,
});
final double? width;
final double? height;
final CutCorner cutCorner;
final double? cutSize;
final Widget? child;
@override
Widget build(BuildContext context) {
return ClipPath(
clipper: EqualCutCornerClipper(
cutSize: cutSize ?? 35,
corner: cutCorner,
),
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: Colors.white,
),
child: child,
),
);
}
}
/// 长方形缺一个角(等边缺角)
class EqualCutCornerClipper extends CustomClipper<Path> {
final double cutSize; // 缺角两条直角边的长度(相等)
final CutCorner corner;
const EqualCutCornerClipper({
this.cutSize = 20,
this.corner = CutCorner.topRight,
});
@override
Path getClip(Size size) {
final path = Path();
final w = size.width;
final h = size.height;
final c = cutSize;
switch (corner) {
case CutCorner.topRight:
path.moveTo(0, 0);
path.lineTo(w - c, 0); // 上边到缺角起点
path.lineTo(w, c); // 🔥 45° 斜线(两条边相等)
path.lineTo(w, h);
path.lineTo(0, h);
break;
case CutCorner.topLeft:
path.moveTo(c, 0);
path.lineTo(w, 0);
path.lineTo(w, h);
path.lineTo(0, h);
path.lineTo(0, c);
break;
case CutCorner.bottomRight:
path.moveTo(0, 0);
path.lineTo(w, 0);
path.lineTo(w, h - c);
path.lineTo(w - c, h);
path.lineTo(0, h);
break;
case CutCorner.bottomLeft:
path.moveTo(0, 0);
path.lineTo(w, 0);
path.lineTo(w, h);
path.lineTo(c, h);
path.lineTo(0, h - c);
break;
}
path.close();
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
if (oldClipper is EqualCutCornerClipper) {
return oldClipper.cutSize != cutSize || oldClipper.corner != corner;
}
return true;
}
}
enum CutCorner { topLeft, topRight, bottomLeft, bottomRight }
+82
View File
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:novyronst/root/lib_export.dart';
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
class NyProgressWidget extends StatelessWidget {
const NyProgressWidget({super.key,
this.progress = 0.4,
this.width,
});
final double progress;
final double? width;
static final double _width = AppScreen.screenWidth - 50;
@override
Widget build(BuildContext context) {
return Stack(
children: [
SizedBox(
height: 14,
width: width ?? _width
),
Positioned(
left: 0, right: 0, top: 4.5,
child: Container(
height: 5,
padding: EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(2.5),
),
child: Row(
children: [
Expanded(
flex: (progress * 100).toInt(),
child: _line(),
),
Expanded(
flex: 100 - (progress * 100).toInt(),
child: _line(isActive: false),
)
],
),
),
),
Positioned(
top: 0, left: (width ?? _width - 50) * progress,
child: _pTag(),
)
],
);
}
Container _line({bool isActive = true}) {
return Container(
height: 2,
decoration: BoxDecoration(
color: isActive ? ConstColor.btGreen : ConstColor.btGreen.withAlpha(127),
),
);
}
Widget _pTag() {
return Container(
height: 14,
padding: EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: ConstColor.btGreen,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
bottomLeft: Radius.circular(4),
topRight: Radius.circular(16),
bottomRight: Radius.circular(4),
)
),
child: NovyText('${(progress * 100).toInt()}%',
fontSize: 10,
color: Colors.black,
),
);
}
}
+55
View File
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:novyronst/common/const/const_color.dart';
import 'package:novyronst/common/help/help_image.dart';
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
class NyTagWidget extends StatelessWidget {
const NyTagWidget({super.key,
required this.text,
this.bgColor,
this.textColor,
});
final String text;
final Color? bgColor;
final Color? textColor;
@override
Widget build(BuildContext context) {
return Container(
height: 20,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: bgColor ?? ConstColor.withValue('EEEEEE'),
borderRadius: BorderRadius.circular(4),
),
child: Center(
child: NovyText(text,
fontSize: 10,
color: textColor ?? ConstColor.black,
),
),
);
}
}
class NyHotWidget extends StatelessWidget {
const NyHotWidget({super.key,
required this.text,
});
final String text;
@override
Widget build(BuildContext context) {
return Row(
children: [
HelpImage.asset(ConstImage.hot, width: 12),
NovyText(text,
fontSize: 10,
color: Colors.black,
)
],
);
}
}