feat: 视频详情页,分类页。个人中心

This commit is contained in:
chengsiwen committed 2026-07-07 16:04:15 +08:00
1 parent b5bce6edcf
commit a3426831ed
26 files changed
+2878 -1077

No files matched your search

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:novyronst/common/help/app_nav.dart';
import 'package:novyronst/common/help/help_image.dart';
import 'package:novyronst/common/model/ny_drama_model.dart';
import 'package:novyronst/pages/first/player_detail/video_detail_page.dart';
import 'package:novyronst/pages/widgets/ny_tag_widget.dart';
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
import '../../../tools/util/app_screen.dart';
import '../../../tools/widgets/novy_button_bg.dart';
import '../../widgets/cut_corner_clipper.dart';
import 'home_remaining_sections.dart';
class HomeCardWidget extends StatelessWidget {
const HomeCardWidget({
super.key,
required this.dramaList,
required this.title,
});
final List<NyDramaModel> dramaList;
final String title;
@override
Widget build(BuildContext context) {
return CutCornerClipper(
width: AppScreen.screenWidth - 32,
height: 308.h,
cutCorner: CutCorner.topRight,
cutSize: 30,
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_threeLine(),
const SizedBox(height: 10),
OutlinedTitle(title, fontSize: 17),
const SizedBox(height: 10),
Expanded(child: _listView()),
const SizedBox(height: 6),
NovyButtonBg(text: 'Click to watch', width: 327.w),
],
),
),
);
}
Widget _listView() {
return ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: dramaList.length,
itemBuilder: (context, index) {
final drama = dramaList[index];
var tag = '';
if (drama.category != null && drama.category!.isNotEmpty) {
tag = drama.category!.first;
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => AppNav.to(VideoDetailPage(drama: drama)),
child: Container(
margin: const EdgeInsets.only(right: 7),
width: 104.w,
child: Column(
children: [
HelpImage.network(drama.imageUrl, width: 104.w, height: 157.h),
const SizedBox(height: 5),
NovyText(
drama.name,
fontSize: 12.sp,
color: Colors.black,
maxLines: 2,
),
const SizedBox(height: 2),
if (tag != '') Row(children: [NyTagWidget(text: tag)]),
],
),
),
);
},
);
}
Widget _threeLine() {
return Row(
children: List.generate(3, (index) {
return Container(
width: 3,
height: 8,
color: Colors.black,
margin: const EdgeInsets.only(right: 3),
);
}),
);
}
}
+47 -36
View File
@@ -1,19 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:novyronst/common/help/app_nav.dart';
import 'package:novyronst/common/model/ny_category_model.dart';
import 'package:novyronst/pages/first/pages/category_list_page.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/extend_widget.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,
required this.categories,
});
const HomeFirstView({super.key, required this.categories});
final List<NyCategoryModel> categories;
@@ -24,21 +24,22 @@ class HomeFirstView extends StatefulWidget {
}
class _HomeFirstViewState extends State<HomeFirstView> {
bool _hasHistory = true;
@override
Widget build(BuildContext context) {
const double _space = 25;
const double space = 25;
return Stack(
children: [
// 151
SizedBox(
height: 151 - _space + 200 + 52 + 20,
SizedBox(height: 151 - space + 200 + 52 + 20),
HelpImage.asset(
ConstImage.homeFirst,
width: AppScreen.screenWidth,
fit: BoxFit.fill,
),
HelpImage.asset(ConstImage.homeFirst, width: AppScreen.screenWidth, fit: BoxFit.fill),
Positioned(
top: 151-_space, left: 16, right: 16,
top: 151 - space,
left: 16,
right: 16,
child: Column(
children: [
CutCornerClipper(
@@ -52,40 +53,46 @@ class _HomeFirstViewState extends State<HomeFirstView> {
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(widget.categories.length, (index) {
final _item = widget.categories[index];
final item = widget.categories[index];
return Container(
margin: const EdgeInsets.only(right: 8),
child: NovyImgBg(imgBg: ConstImage.cateBg,
child: NovyImgBg(
imgBg: ConstImage.cateBg,
height: 52,
child: Stack(
children:[
NovyText(_item.name, color: ConstColor.btGreen,
children: [
NovyText(
item.name,
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
blurRadius: 0, // Blur: 0
color: Colors.black, // #000000
),
),
Positioned(
bottom: 0, left: 0, right: 0,
child: Container(
height: 2,
color: Colors.black,
),
)
]
bottom: 0,
left: 0,
right: 0,
child: Container(height: 2, color: Colors.black),
),
],
),
),
).addInkWell(
onTap: () {
AppNav.to(CategoryListPage(preCategory: item));
},
);
}),
),
),
],
)
)
),
),
],
);
}
@@ -97,13 +104,14 @@ class _HomeFirstViewState extends State<HomeFirstView> {
children: [
Row(
children: [
NovyText('Current Viewing Progress',
NovyText(
'Current Viewing Progress',
color: Colors.black,
fontSize: 16,
fontFamily: ConstFont.montBlack,
),
const SizedBox(width: 10),
HelpImage.asset(ConstImage.homeCurrentTag, height: 10)
HelpImage.asset(ConstImage.homeCurrentTag, height: 10),
],
),
const SizedBox(height: 10),
@@ -111,7 +119,8 @@ class _HomeFirstViewState extends State<HomeFirstView> {
children: [
Container(
color: Colors.grey.withAlpha(200),
width: 60, height: 90,
width: 60,
height: 90,
),
const SizedBox(width: 12),
SizedBox(
@@ -119,13 +128,15 @@ class _HomeFirstViewState extends State<HomeFirstView> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NovyText('Jdksyjs dsaoy',
NovyText(
'Jdksyjs dsaoy',
color: Colors.black,
fontSize: 14,
fontFamily: ConstFont.montMedium,
),
const SizedBox(height: 4),
NovyText('Jdksyjs dsaoy dasku dsaldsa dljas',
NovyText(
'Jdksyjs dsaoy dasku dsaldsa dljas',
color: Colors.grey,
fontSize: 10,
fontFamily: ConstFont.montRegular,
@@ -138,18 +149,18 @@ class _HomeFirstViewState extends State<HomeFirstView> {
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,)
NovyButtonBg(text: 'Keep Watching', type: 2, width: 327.w),
],
),
);
}
}
}
+89 -84
View File
@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:novyronst/common/const/const_color.dart';
import 'package:novyronst/common/const/const_string.dart';
import 'package:novyronst/common/help/app_nav.dart';
import 'package:novyronst/common/help/help_image.dart';
import 'package:novyronst/common/model/ny_drama_model.dart';
import 'package:novyronst/pages/first/player_detail/video_detail_page.dart';
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
import '../../../tools/util/app_screen.dart';
@@ -12,11 +14,7 @@ import '../../widgets/cut_corner_clipper.dart';
import '../../widgets/ny_tag_widget.dart';
class HomeRankWidget extends StatelessWidget {
const HomeRankWidget({
super.key,
required this.dramaRanks,
});
const HomeRankWidget({super.key, required this.dramaRanks});
final List<NyDramaModel> dramaRanks;
@@ -28,24 +26,25 @@ class HomeRankWidget extends StatelessWidget {
cutSize: 40,
child: Padding(
padding: const EdgeInsets.all(8),
child: dramaRanks.isEmpty ? SizedBox(height: 20,) : Column(
children: [
_buildRankList(),
const SizedBox(height: 5),
Row(
children: [
SizedBox(width: 20.w),
NovyButtonBg(text: 'Keep Watching', width: 308.w,)
],
)
],
),
child: dramaRanks.isEmpty
? const SizedBox(height: 20)
: Column(
children: [
_buildRankList(),
const SizedBox(height: 5),
Row(
children: [
SizedBox(width: 20.w),
NovyButtonBg(text: 'Keep Watching', width: 308.w),
],
),
],
),
),
);
}
Row _buildRankList() {
return Row(
children: [
_firstVideoView(),
@@ -56,85 +55,91 @@ class HomeRankWidget extends StatelessWidget {
}
Widget _firstVideoView() {
NyDramaModel _item = dramaRanks.first;
String _tag = "Safiray";
if (_item.categoryList!=null && _item.categoryList!.isNotEmpty) {
_tag = _item.categoryList!.first.name;
final item = dramaRanks.first;
var tag = 'Safiray';
if (item.categoryList != null && item.categoryList!.isNotEmpty) {
tag = item.categoryList!.first.name;
}
return SizedBox(
width: 160.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
SizedBox(
width: 160.w, height: 240.w,
child: HelpImage.network(_item.imageUrl, fit: BoxFit.cover),
),
Positioned(
top: -10, left: 1,
child: _colorsText(1),
)
],
),
const SizedBox(height: 10),
NovyText(_item.name, fontSize: 15,
color: Colors.black,
fontFamily: ConstFont.montMedium,
),
const SizedBox(height: 5),
Row(
children: [
NyTagWidget(text: _tag),
],
),
const SizedBox(height: 8),
],
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => AppNav.to(VideoDetailPage(drama: item)),
child: SizedBox(
width: 160.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
SizedBox(
width: 160.w,
height: 240.w,
child: HelpImage.network(item.imageUrl, fit: BoxFit.cover),
),
Positioned(top: -10, left: 1, child: _colorsText(1)),
],
),
const SizedBox(height: 10),
NovyText(
item.name,
fontSize: 15,
color: Colors.black,
fontFamily: ConstFont.montMedium,
),
const SizedBox(height: 5),
Row(children: [NyTagWidget(text: tag)]),
const SizedBox(height: 8),
],
),
),
);
}
Widget _otherVideoView() {
int _count = dramaRanks.length - 1;
if (_count > 3) _count = 3;
var count = dramaRanks.length - 1;
if (count > 3) count = 3;
return Column(
children: List.generate(_count, (index) {
NyDramaModel _item = dramaRanks[index + 1];
String _tag = "";
if (_item.categoryList!=null && _item.categoryList!.isNotEmpty) {
_tag = _item.categoryList!.first.name;
children: List.generate(count, (index) {
final item = dramaRanks[index + 1];
var tag = '';
if (item.categoryList != null && item.categoryList!.isNotEmpty) {
tag = item.categoryList!.first.name;
}
return Container(
margin: const EdgeInsets.only(bottom: 8),
width: 160.w, height: 93.w,
child: Row(
children: [
HelpImage.network(_item.imageUrl, width: 62.w, height: 93.w),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_colorsText(index + 2),
const SizedBox(height: 8),
NovyText(_item.name, fontSize: 13,
color: Colors.black,
),
const SizedBox(height: 5),
_tag.isNotEmpty ? NyTagWidget(text: _tag) : SizedBox()
],
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => AppNav.to(VideoDetailPage(drama: item)),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
width: 160.w,
height: 93.w,
child: Row(
children: [
HelpImage.network(item.imageUrl, width: 62.w, height: 93.w),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_colorsText(index + 2),
const SizedBox(height: 8),
NovyText(item.name, fontSize: 13, color: Colors.black),
const SizedBox(height: 5),
tag.isNotEmpty
? NyTagWidget(text: tag)
: const SizedBox(),
],
),
),
)
],
],
),
),
);
})
}),
);
}
_colorsText(int index) {
Widget _colorsText(int index) {
if (index == 4) {
return Text(
'$index',
@@ -160,7 +165,7 @@ class HomeRankWidget extends StatelessWidget {
child: Text(
'$index',
style: TextStyle(
fontSize: index==1 ? 40: 20,
fontSize: index == 1 ? 40 : 20,
fontWeight: FontWeight.bold,
color: Colors.white,
fontStyle: FontStyle.italic,
@@ -168,4 +173,4 @@ class HomeRankWidget extends StatelessWidget {
),
);
}
}
}
@@ -1,15 +1,54 @@
import 'package:flutter/material.dart';
import 'package:novyronst/common/api/video_request.dart';
import 'package:novyronst/common/const/const_color.dart';
import 'package:novyronst/common/const/const_string.dart';
import 'package:novyronst/common/help/app_nav.dart';
import 'package:novyronst/common/help/help_image.dart';
import 'package:novyronst/common/model/ny_drama_model.dart';
import 'package:novyronst/common/model/ny_episode_model.dart';
import 'package:novyronst/pages/first/player_detail/video_detail_page.dart';
import 'package:novyronst/pages/widgets/cut_corner_clipper.dart';
import 'package:novyronst/pages/widgets/ny_tag_widget.dart';
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
class HomeRemainingSections extends StatelessWidget {
const HomeRemainingSections({super.key});
import '../../../common/model/ny_detail_model.dart';
const int episodeSeriesPageSize = 6;
static const _ActorInfo _actor = _ActorInfo(
class HomeRemainingSections extends StatefulWidget {
const HomeRemainingSections({super.key, required this.dramaOne});
final NyDramaModel dramaOne;
@override
State<StatefulWidget> createState() {
return _HomeRemainingSectionsState();
}
}
class _HomeRemainingSectionsState extends State<HomeRemainingSections> {
NyDetailModel? _detailModel;
@override
void initState() {
super.initState();
_getDetailsData();
}
void _getDetailsData() {
VideoRequest.getVideoDetail(
shortPlayId: widget.dramaOne.shortPlayId ?? 0,
shortPlayVideoId: widget.dramaOne.shortPlayVideoId ?? 0,
).then((value) {
if (value != null) {
setState(() {
_detailModel = value;
});
}
});
}
final _ActorInfo _actor = _ActorInfo(
name: 'Cheng Zi',
tags: 'Romantic Lead | Revenge Drama | Lead Actor',
bornYear: '2002',
@@ -17,164 +56,32 @@ class HomeRemainingSections extends StatelessWidget {
nationality: 'China',
);
static const List<_DramaItem> _seriesItems = [
_DramaItem(
title: 'Cities Of Smoke Starlight',
category: 'Satisfying',
palette: [Color(0xFF33241D), Color(0xFFD8A65B)],
),
_DramaItem(
title: 'Cities Of Smoke Starlight',
category: 'Satisfying',
palette: [Color(0xFF25354C), Color(0xFFC88B57)],
),
_DramaItem(
title: 'Cities Of Smoke Starlight',
category: 'Satisfying',
palette: [Color(0xFF3E2B2C), Color(0xFFE2B36B)],
),
_DramaItem(
title: 'Cities Of Smoke Starlight',
category: 'Satisfying',
palette: [Color(0xFF443A2C), Color(0xFFC48645)],
),
_DramaItem(
title: 'Cities Of Smoke Starlight',
category: 'Satisfying',
palette: [Color(0xFF314559), Color(0xFFE5AA65)],
),
_DramaItem(
title: 'Cities Of Smoke Starlight',
category: 'Satisfying',
palette: [Color(0xFF2C2637), Color(0xFFD39458)],
),
];
static const List<String> _categoryTitles = [
'Counterattack Life',
'Reborn And Returned',
'God Of War Returns',
];
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 92),
child: Column(
padding: const EdgeInsets.fromLTRB(16, 3, 16, 16),
child: Stack(
children: [
const SizedBox(height: 10),
const _ActorSection(actor: _actor, items: _seriesItems),
const SizedBox(height: 18),
..._categoryTitles.map((title) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
child: _DramaShelfSection(title: title, items: _seriesItems),
);
}),
Container(
margin: const EdgeInsets.only(top: 12),
child: _ActorSection(actor: _actor, detailModel: _detailModel),
),
Positioned(
left: -10,
top: 0,
child: HelpImage.asset(ConstImage.homeEyes, width: 72),
),
],
),
);
}
}
class _TopRankingPoster extends StatelessWidget {
const _TopRankingPoster({required this.item});
final _DramaItem item;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Stack(
children: [
Positioned.fill(
child: _PosterBlock(item: item, showLargeTitle: true),
),
Positioned(
top: 4,
left: 5,
child: NovyText(
'1',
color: ConstColor.btGreen,
fontSize: 36,
fontFamily: ConstFont.montBlack,
shadow: const Shadow(
offset: Offset(1.5, 1.5),
color: Colors.black,
),
),
),
],
),
),
const SizedBox(height: 8),
NovyText(
item.title,
color: Colors.black,
fontSize: 14,
fontFamily: ConstFont.montMedium,
),
const SizedBox(height: 5),
NyTagWidget(text: item.category),
],
);
}
}
class _RankingListItem extends StatelessWidget {
const _RankingListItem({required this.rank, required this.item});
final int rank;
final _DramaItem item;
@override
Widget build(BuildContext context) {
final Color rankColor = rank == 4
? const Color(0xFFBBBBBB)
: ConstColor.btGreen;
return Row(
children: [
SizedBox(width: 42, height: 64, child: _PosterBlock(item: item)),
const SizedBox(width: 6),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
NovyText(
'$rank',
color: rankColor,
fontSize: 19,
fontFamily: ConstFont.montBlack,
lineHeight: 1,
),
const SizedBox(height: 3),
NovyText(
item.title,
color: Colors.black,
fontSize: 11,
fontFamily: ConstFont.montBold,
maxLines: 1,
),
const SizedBox(height: 4),
NyTagWidget(text: item.category),
],
),
),
],
);
}
}
class _ActorSection extends StatelessWidget {
const _ActorSection({required this.actor, required this.items});
const _ActorSection({required this.actor, required this.detailModel});
final _ActorInfo actor;
final List<_DramaItem> items;
final NyDetailModel? detailModel;
@override
Widget build(BuildContext context) {
@@ -184,26 +91,43 @@ class _ActorSection extends StatelessWidget {
child: Stack(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(10, 18, 10, 10),
padding: const EdgeInsets.fromLTRB(10, 16, 10, 10),
child: Column(
children: [
const _CornerEyes(),
const SizedBox(height: 4),
_OutlinedTitle('Read This Series Of Short Plays', fontSize: 19),
// const SizedBox(height: 10),
Align(
alignment: Alignment.topRight,
child: OutlinedTitle(
'Read This Series Of Short Plays',
fontSize: 17,
),
),
const SizedBox(height: 26),
const _ActorAvatar(),
Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFFFA6D9), Color(0xFFFFE6F5)],
),
),
child: HelpImage.asset(ConstImage.homeHead, width: 100),
),
const SizedBox(height: 10),
NovyText(
actor.name,
color: Colors.black,
fontSize: 22,
fontSize: 18,
fontFamily: ConstFont.montBlack,
),
const SizedBox(height: 10),
NovyText(
actor.tags,
color: const Color(0xFF888888),
fontSize: 13,
fontSize: 12,
fontFamily: ConstFont.montRegular,
),
const SizedBox(height: 12),
@@ -211,9 +135,7 @@ class _ActorSection extends StatelessWidget {
const SizedBox(height: 8),
_SectionHeader(title: 'Series'),
const SizedBox(height: 8),
_SeriesGrid(items: items, showTags: false),
const SizedBox(height: 12),
const _PagerDots(),
_SeriesGrid(detailModel: detailModel),
],
),
),
@@ -223,147 +145,144 @@ class _ActorSection extends StatelessWidget {
}
}
class _DramaShelfSection extends StatelessWidget {
const _DramaShelfSection({required this.title, required this.items});
class _SeriesGrid extends StatefulWidget {
const _SeriesGrid({required this.detailModel});
final String title;
final List<_DramaItem> items;
final NyDetailModel? detailModel;
@override
Widget build(BuildContext context) {
return CutCornerClipper(
cutSize: 40,
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 14, 8, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _GripBars(),
const SizedBox(height: 8),
_OutlinedTitle(title, fontSize: 19),
const SizedBox(height: 12),
Row(
children: List.generate(3, (index) {
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: index == 2 ? 0 : 8),
child: _SeriesCard(item: items[index], showTag: true),
),
);
}),
),
const SizedBox(height: 8),
const _GreenActionButton(text: 'Click to watch'),
],
),
),
);
}
State<_SeriesGrid> createState() => _SeriesGridState();
}
class _SeriesGrid extends StatelessWidget {
const _SeriesGrid({required this.items, required this.showTags});
class _SeriesGridState extends State<_SeriesGrid> {
final PageController _pageController = PageController();
int _currentPage = 0;
final List<_DramaItem> items;
final bool showTags;
List<NyEpisodeModel> get _items => widget.detailModel?.episodeList ?? [];
String get _coverUrl {
final shortPlayImage = widget.detailModel?.shortPlayInfo?.imageUrl ?? '';
if (shortPlayImage.isNotEmpty) return shortPlayImage;
return widget.detailModel?.videoInfo?.imageUrl ?? '';
}
@override
void didUpdateWidget(covariant _SeriesGrid oldWidget) {
super.didUpdateWidget(oldWidget);
final pageCount = episodeSeriesPageCount(_items.length);
if (_currentPage >= pageCount) {
_currentPage = pageCount - 1;
if (_pageController.hasClients) {
_pageController.jumpToPage(_currentPage);
}
}
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GridView.builder(
padding: EdgeInsets.zero,
itemCount: items.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 9,
mainAxisSpacing: 10,
childAspectRatio: 0.47,
),
itemBuilder: (context, index) {
return _SeriesCard(item: items[index], showTag: showTags);
final pageCount = episodeSeriesPageCount(_items.length);
return LayoutBuilder(
builder: (context, constraints) {
final gridHeight = 200.0 * 2;
return Column(
children: [
SizedBox(
height: gridHeight,
child: PageView.builder(
controller: _pageController,
itemCount: pageCount,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
itemBuilder: (context, pageIndex) {
final pageItems = episodeSeriesPageItems(_items, pageIndex);
return GridView.builder(
padding: EdgeInsets.zero,
itemCount: pageItems.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 9,
mainAxisSpacing: 10,
childAspectRatio: 104 / 190,
),
itemBuilder: (context, index) {
final item = pageItems[index];
return _SeriesCard(
item: item,
coverUrl: _coverUrl,
onTap: () => _openDetail(item),
);
},
);
},
),
),
const SizedBox(height: 12),
_PagerDots(count: pageCount, currentIndex: _currentPage),
],
);
},
);
}
void _openDetail(NyEpisodeModel item) {
final detail = widget.detailModel;
if (detail == null) return;
final videos = buildEpisodeDramaList(detail);
if (videos.isEmpty) return;
final selected = videos.firstWhere(
(video) => video.shortPlayVideoId == item.shortPlayVideoId,
orElse: () => videos.first,
);
AppNav.to(VideoDetailPage(drama: selected));
}
}
class _SeriesCard extends StatelessWidget {
const _SeriesCard({required this.item, required this.showTag});
const _SeriesCard({
required this.item,
required this.coverUrl,
required this.onTap,
});
final _DramaItem item;
final bool showTag;
final NyEpisodeModel item;
final String coverUrl;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 0.68,
child: _PosterBlock(item: item, showLargeTitle: true),
),
const SizedBox(height: 7),
NovyText(
item.title,
color: Colors.black,
fontSize: 14,
lineHeight: 1.05,
fontFamily: ConstFont.montMedium,
maxLines: 2,
),
if (showTag) ...[
const SizedBox(height: 7),
NyTagWidget(text: item.category),
],
],
);
}
}
class _PosterBlock extends StatelessWidget {
const _PosterBlock({required this.item, this.showLargeTitle = false});
final _DramaItem item;
final bool showLargeTitle;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: item.palette,
),
),
child: Stack(
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Positioned(
top: 12,
left: 10,
right: 10,
child: Container(
height: showLargeTitle ? 74 : 28,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(80),
),
),
AspectRatio(
aspectRatio: 0.68,
child: HelpImage.network(coverUrl, fit: BoxFit.cover),
),
Positioned(
left: 7,
right: 7,
bottom: 10,
child: NovyText(
showLargeTitle ? 'CITIES\nOF SMOKE\nSTARLIGHT' : item.title,
color: Colors.white,
fontSize: showLargeTitle ? 17 : 8,
lineHeight: 0.95,
textAlign: TextAlign.center,
fontFamily: ConstFont.montBlack,
maxLines: showLargeTitle ? 3 : 2,
shadow: const Shadow(offset: Offset(1, 1), color: Colors.black54),
),
const SizedBox(height: 7),
NovyText(
'Episode ${item.episode}',
color: Colors.black,
fontSize: 14,
lineHeight: 1.05,
fontFamily: ConstFont.montMedium,
),
],
),
@@ -371,37 +290,6 @@ class _PosterBlock extends StatelessWidget {
}
}
class _ActorAvatar extends StatelessWidget {
const _ActorAvatar();
@override
Widget build(BuildContext context) {
return Container(
width: 116,
height: 116,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFFFA6D9), Color(0xFFFFE6F5)],
),
),
child: Center(
child: Container(
width: 58,
height: 58,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.55),
borderRadius: BorderRadius.circular(18),
),
child: const Icon(Icons.person, size: 44, color: Color(0xFF8E2A55)),
),
),
);
}
}
class _ActorStats extends StatelessWidget {
const _ActorStats({required this.actor});
@@ -429,14 +317,14 @@ class _ActorStats extends StatelessWidget {
NovyText(
stats[index][0],
color: Colors.black,
fontSize: 18,
fontSize: 16,
fontFamily: ConstFont.montMedium,
),
const SizedBox(height: 7),
NovyText(
stats[index][1],
color: const Color(0xFF777777),
fontSize: 13,
fontSize: 12,
fontFamily: ConstFont.montRegular,
),
],
@@ -467,8 +355,8 @@ class _SectionHeader extends StatelessWidget {
}
}
class _OutlinedTitle extends StatelessWidget {
const _OutlinedTitle(this.text, {required this.fontSize});
class OutlinedTitle extends StatelessWidget {
const OutlinedTitle(this.text, {super.key, required this.fontSize});
final String text;
final double fontSize;
@@ -505,151 +393,29 @@ class _OutlinedTitle extends StatelessWidget {
}
}
class _GreenActionButton extends StatelessWidget {
const _GreenActionButton({required this.text});
final String text;
@override
Widget build(BuildContext context) {
return ClipPath(
clipper: const EqualCutCornerClipper(
cutSize: 18,
corner: CutCorner.bottomLeft,
),
child: Container(
height: 34,
decoration: BoxDecoration(
color: ConstColor.btGreen,
border: Border.all(color: Colors.black, width: 2),
),
child: Stack(
children: [
const Positioned(left: 22, top: 6, child: _GripBars()),
const Positioned(right: 10, bottom: 4, child: _GripBars()),
Center(
child: NovyText(
text,
color: Colors.black,
fontSize: 14,
fontFamily: ConstFont.montBlack,
),
),
],
),
),
);
}
}
class _CornerEyes extends StatelessWidget {
const _CornerEyes();
@override
Widget build(BuildContext context) {
return SizedBox(
height: 18,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: -3,
top: -38,
child: Transform.rotate(
angle: 0.68,
child: Container(
width: 34,
height: 84,
color: ConstColor.btGreen,
),
),
),
const Positioned(left: 18, top: -12, child: _EyeDot()),
const Positioned(left: 38, top: -18, child: _EyeDot()),
],
),
);
}
}
class _EyeDot extends StatelessWidget {
const _EyeDot();
@override
Widget build(BuildContext context) {
return Container(
width: 22,
height: 26,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Align(
alignment: Alignment.topRight,
child: Container(
width: 10,
height: 10,
margin: const EdgeInsets.only(top: 2, right: 2),
decoration: const BoxDecoration(
color: Colors.black,
shape: BoxShape.circle,
),
),
),
);
}
}
class _PagerDots extends StatelessWidget {
const _PagerDots();
const _PagerDots({required this.count, required this.currentIndex});
final int count;
final int currentIndex;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(7, (index) {
children: List.generate(count, (index) {
final active = index == currentIndex;
return Container(
width: index == 1 ? 26 : 7,
width: active ? 26 : 7,
height: 7,
margin: const EdgeInsets.symmetric(horizontal: 3),
color: index == 1 ? ConstColor.btGreen : const Color(0xFFD9D9D9),
color: active ? ConstColor.btGreen : const Color(0xFFD9D9D9),
);
}),
);
}
}
class _GripBars extends StatelessWidget {
const _GripBars();
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(4, (index) {
return Container(
width: 3,
height: 8,
margin: const EdgeInsets.only(right: 2),
color: Colors.black,
);
}),
);
}
}
class _DramaItem {
const _DramaItem({
required this.title,
required this.category,
required this.palette,
});
final String title;
final String category;
final List<Color> palette;
}
class _ActorInfo {
const _ActorInfo({
required this.name,
@@ -665,3 +431,18 @@ class _ActorInfo {
final String height;
final String nationality;
}
int episodeSeriesPageCount(int itemCount) {
if (itemCount <= 0) return 1;
return (itemCount / episodeSeriesPageSize).ceil();
}
List<NyEpisodeModel> episodeSeriesPageItems(
List<NyEpisodeModel> items,
int pageIndex,
) {
final start = pageIndex * episodeSeriesPageSize;
if (start >= items.length) return [];
final end = (start + episodeSeriesPageSize).clamp(0, items.length);
return items.sublist(start, end);
}
+140 -98
View File
@@ -1,18 +1,20 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:novyronst/root/lib_export.dart';
import 'package:novyronst/tools/widgets/extend_widget.dart';
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
class HomeSearchView extends StatefulWidget {
const HomeSearchView({super.key,
const HomeSearchView({
super.key,
this.onlyTap = false,
this.onTap,
this.showLogo = true,
});
final bool onlyTap;
final Function()? onTap;
final bool showLogo;
@override
State<StatefulWidget> createState() {
@@ -21,66 +23,92 @@ class HomeSearchView extends StatefulWidget {
}
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
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),
if (widget.showLogo) ...[
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:widget.onlyTap
? NovyText('What are...... ',
fontSize: 13,
color: Colors.white.withAlpha(127),
)
: 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)
],
child: SizedBox(
height: 36,
child: Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: HelpImage.asset(
ConstImage.searchBg,
height: 36,
fit: BoxFit.fill,
),
),
)
],
Padding(
padding: const EdgeInsets.only(left: 11, right: 78),
child: Row(
children: [
const Icon(
Icons.search,
color: Colors.white,
size: 24,
),
Expanded(
child: widget.onlyTap
? NovyText(
'What are...... ',
fontSize: 13,
color: Colors.white.withAlpha(127),
)
: CupertinoTextField(
controller: _searchController,
// focusNode: widget.focusNode,
decoration: const BoxDecoration(),
placeholder: 'What are...... ',
placeholderStyle: TextStyle(
fontSize: 13,
color: Colors.white.withAlpha(127),
),
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
textInputAction: TextInputAction.search,
onEditingComplete: () {},
),
),
],
),
),
Positioned(
top: 0,
right: 0,
bottom: 0,
child: HelpImage.asset(
ConstImage.homeSearch,
width: 78,
height: 36,
fit: BoxFit.fill,
),
),
],
),
),
)
),
],
),
),
@@ -88,11 +116,10 @@ class _SearchViewState extends State<HomeSearchView> {
}
}
/// 搜索页面的搜索框
class SeSearchView extends StatefulWidget {
const SeSearchView({super.key,
const SeSearchView({
super.key,
this.onClickSearch,
required this.controller,
this.focusNode,
@@ -109,62 +136,77 @@ class SeSearchView extends StatefulWidget {
}
class _SeSearchViewState extends State<SeSearchView> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
height: 44,
padding: EdgeInsets.only(top: 4, bottom: 4),
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: widget.controller,
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: () {
if (widget.controller.text.trim().isNotEmpty) {
widget.onClickSearch?.call(widget.controller.text);
}
},
),),
HelpImage.asset(ConstImage.homeSearch,
width: 78, height: 36, fit: BoxFit.fill
).addInkWell(onTap: () {
if (widget.controller.text.trim().isNotEmpty) {
widget.onClickSearch?.call(widget.controller.text);
}
})
],
padding: const EdgeInsets.only(top: 4, bottom: 4),
child: SizedBox(
height: 36,
child: Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: HelpImage.asset(
ConstImage.searchBg,
height: 36,
fit: BoxFit.fill,
),
),
)
],
)
Padding(
padding: const EdgeInsets.only(left: 11, right: 78),
child: Row(
children: [
const Icon(Icons.search, color: Colors.white, size: 24),
Expanded(
child: CupertinoTextField(
controller: widget.controller,
focusNode: widget.focusNode,
decoration: const BoxDecoration(),
placeholder: 'What are...... ',
placeholderStyle: TextStyle(
fontSize: 13,
color: Colors.white.withAlpha(127),
),
style: TextStyle(fontSize: 14, color: Colors.white),
textInputAction: TextInputAction.search,
onEditingComplete: () {
if (widget.controller.text.trim().isNotEmpty) {
widget.onClickSearch?.call(widget.controller.text);
}
},
),
),
],
),
),
Positioned(
top: 0,
right: 0,
bottom: 0,
child:
HelpImage.asset(
ConstImage.homeSearch,
width: 78,
height: 36,
fit: BoxFit.fill,
).addInkWell(
onTap: () {
if (widget.controller.text.trim().isNotEmpty) {
widget.onClickSearch?.call(widget.controller.text);
}
},
),
),
],
),
),
);
}
}
}