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';
|
||||
}
|
||||
@@ -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,)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user