diff --git a/assets/imgs/remove.png b/assets/imgs/remove.png new file mode 100644 index 0000000..ebcfc98 Binary files /dev/null and b/assets/imgs/remove.png differ diff --git a/assets/other/about.png b/assets/other/about.png new file mode 100644 index 0000000..45d482a Binary files /dev/null and b/assets/other/about.png differ diff --git a/assets/other/copy.png b/assets/other/copy.png new file mode 100644 index 0000000..778746e Binary files /dev/null and b/assets/other/copy.png differ diff --git a/assets/other/history.png b/assets/other/history.png new file mode 100644 index 0000000..c46d41a Binary files /dev/null and b/assets/other/history.png differ diff --git a/assets/other/mine_bg.png b/assets/other/mine_bg.png new file mode 100644 index 0000000..d2b2484 Binary files /dev/null and b/assets/other/mine_bg.png differ diff --git a/assets/other/mine_card_bg.png b/assets/other/mine_card_bg.png new file mode 100644 index 0000000..7ce7f6d Binary files /dev/null and b/assets/other/mine_card_bg.png differ diff --git a/assets/other/settings.png b/assets/other/settings.png new file mode 100644 index 0000000..8dc664f Binary files /dev/null and b/assets/other/settings.png differ diff --git a/lib/common/help/help_image.dart b/lib/common/help/help_image.dart index 912859e..7efcc2e 100644 --- a/lib/common/help/help_image.dart +++ b/lib/common/help/help_image.dart @@ -33,7 +33,18 @@ class ConstImage { // btn_bg_rad1 static const String btnBgRad1 = "assets/imgs/btn_bg_rad1.png"; static const String btnBgRad2 = "assets/imgs/btn_bg_rad2.png"; + // remove + static const String remove = "assets/imgs/remove.png"; + /// other + static const String mineBg = "assets/other/mine_bg.png"; + // mine_card about history settings + static const String mineCard = "assets/other/mine_card.png"; + static const String about = "assets/other/about.png"; + static const String history = "assets/other/history.png"; + static const String settings = "assets/other/settings.png"; + // copy + static const String copy = "assets/other/copy.png"; } diff --git a/lib/pages/first/pages/first_page.dart b/lib/pages/first/pages/first_page.dart index 9c3643a..16b0847 100644 --- a/lib/pages/first/pages/first_page.dart +++ b/lib/pages/first/pages/first_page.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:novyronst/common/help/help_image.dart'; +import 'package:novyronst/common/help/app_nav.dart'; +import 'package:novyronst/pages/first/pages/search_page.dart'; import 'package:novyronst/pages/first/widgets/home_first_view.dart'; +import 'package:novyronst/pages/first/widgets/home_remaining_sections.dart'; import 'package:novyronst/pages/first/widgets/search_view.dart'; +import 'package:novyronst/root/lib_export.dart'; import '../../../tools/widgets/app_bg_page.dart'; @@ -15,30 +18,33 @@ class FirstPage extends StatefulWidget { } class _RootPageState extends State { - @override Widget build(BuildContext context) { return AppBgPage( child: Column( children: [ - HomeSearchView(), + HomeSearchView( + onlyTap: true, + onTap: () { + AppNav.to(SearchPage()); + }, + ), Expanded( child: CustomScrollView( slivers: [ - SliverToBoxAdapter( - child: HomeFirstView(), - ), + SliverToBoxAdapter(child: HomeFirstView()), SliverPadding( - padding: const EdgeInsets.only(left: 16, right: 10, top: 16), - sliver: SliverToBoxAdapter( - child: HelpImage.asset(ConstImage.homeRank), - ) - ) + padding: const EdgeInsets.only(left: 16, right: 10, top: 16), + sliver: SliverToBoxAdapter( + child: HelpImage.asset(ConstImage.homeRank), + ) + ), + const SliverToBoxAdapter(child: HomeRemainingSections()), ], ), - ) + ), ], ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/first/pages/search_page.dart b/lib/pages/first/pages/search_page.dart new file mode 100644 index 0000000..8546c41 --- /dev/null +++ b/lib/pages/first/pages/search_page.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:novyronst/common/const/const_color.dart'; +import 'package:novyronst/pages/first/widgets/recent_history_view.dart'; +import 'package:novyronst/pages/first/widgets/search_view.dart'; +import 'package:novyronst/tools/util/search_history_util.dart'; +import 'package:novyronst/tools/widgets/novy_app_bar.dart'; + +class SearchPage extends StatefulWidget { + const SearchPage({super.key}); + + @override + State createState() { + return _SearchPageState(); + } +} + +class _SearchPageState extends State { + + List _history = []; + final _searchController = TextEditingController(); + + Future _loadSearchHistory() async { + final list = await SearchHistoryUtil.getSearchList(); + if (!mounted) return; + setState(() { + _history = list; + }); + } + + Future _onGoSearch(String keyword,) async { + await SearchHistoryUtil.addSearch(keyword); + await _loadSearchHistory(); + // HelpNav.to(ResultPage( + // keyword: keyword, + // )); + // 发起 搜索 + + } + + Widget _buildHistoryView() { + return _history.isEmpty ? SizedBox() : RecentHistoryView( + searchHistory: _history, + onTapClear: () { + // HelpNav.showDialog(ComConfirmWidget( + // title: 'Clear Search History?', + // )); + _history.clear(); + setState(() {}); + }, + onTapHistory: (value) { + print('object: $value'); + // HelpNav.to(ResultPage(keyword: value)); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: ConstColor.withValue('#110D08'), + appBar: NovyAppBar( + titleWidget: SeSearchView( + onClickSearch: (keyword) => _onGoSearch(keyword), + ), + ), + body: CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.symmetric(vertical: 16), + sliver: SliverToBoxAdapter( + child: _buildHistoryView(), + ) + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/first/pages/search_result_page.dart b/lib/pages/first/pages/search_result_page.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/pages/first/widgets/home_remaining_sections.dart b/lib/pages/first/widgets/home_remaining_sections.dart new file mode 100644 index 0000000..7c92dda --- /dev/null +++ b/lib/pages/first/widgets/home_remaining_sections.dart @@ -0,0 +1,735 @@ +import 'package:flutter/material.dart'; +import 'package:novyronst/common/const/const_color.dart'; +import 'package:novyronst/common/const/const_string.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}); + + static const List<_DramaItem> _rankingItems = [ + _DramaItem( + title: 'Star Of The Ocean', + category: 'Period Drama', + palette: [Color(0xFF6E4968), Color(0xFFE5A3B8)], + ), + _DramaItem( + title: 'Not Your Bride', + category: 'Period Drama', + palette: [Color(0xFF262431), Color(0xFFE0A15F)], + ), + _DramaItem( + title: 'Not Your Bride', + category: 'Period Drama', + palette: [Color(0xFF261B16), Color(0xFFD9C2A2)], + ), + _DramaItem( + title: 'Not Your Bride', + category: 'Period Drama', + palette: [Color(0xFF223E36), Color(0xFFFFC568)], + ), + ]; + + static const _ActorInfo _actor = _ActorInfo( + name: 'Cheng Zi', + tags: 'Romantic Lead | Revenge Drama | Lead Actor', + bornYear: '2002', + height: '168cm', + 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 _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( + children: [ + const _RankingSection(items: _rankingItems), + const SizedBox(height: 18), + 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), + ); + }), + ], + ), + ); + } +} + +class _RankingSection extends StatelessWidget { + const _RankingSection({required this.items}); + + final List<_DramaItem> items; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black, width: 2), + ), + child: Column( + children: [ + SizedBox( + height: 230, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 6, child: _TopRankingPoster(item: items.first)), + const SizedBox(width: 8), + Expanded( + flex: 5, + child: Column( + children: List.generate(3, (index) { + return Expanded( + child: _RankingListItem( + rank: index + 2, + item: items[index + 1], + ), + ); + }), + ), + ), + ], + ), + ), + const SizedBox(height: 5), + const _GreenActionButton(text: 'View more rankings'), + ], + ), + ); + } +} + +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}); + + final _ActorInfo actor; + final List<_DramaItem> items; + + @override + Widget build(BuildContext context) { + return CutCornerClipper( + cutCorner: CutCorner.topLeft, + cutSize: 66, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 18, 10, 10), + child: Column( + children: [ + const _CornerEyes(), + const SizedBox(height: 4), + _OutlinedTitle('Read This Series Of Short Plays', fontSize: 19), + const SizedBox(height: 26), + const _ActorAvatar(), + const SizedBox(height: 10), + NovyText( + actor.name, + color: Colors.black, + fontSize: 22, + fontFamily: ConstFont.montBlack, + ), + const SizedBox(height: 10), + NovyText( + actor.tags, + color: const Color(0xFF888888), + fontSize: 13, + fontFamily: ConstFont.montRegular, + ), + const SizedBox(height: 12), + _ActorStats(actor: actor), + const SizedBox(height: 8), + _SectionHeader(title: 'Series'), + const SizedBox(height: 8), + _SeriesGrid(items: items, showTags: false), + const SizedBox(height: 12), + const _PagerDots(), + ], + ), + ), + ], + ), + ); + } +} + +class _DramaShelfSection extends StatelessWidget { + const _DramaShelfSection({required this.title, required this.items}); + + final String title; + final List<_DramaItem> items; + + @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'), + ], + ), + ), + ); + } +} + +class _SeriesGrid extends StatelessWidget { + const _SeriesGrid({required this.items, required this.showTags}); + + final List<_DramaItem> items; + final bool showTags; + + @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); + }, + ); + } +} + +class _SeriesCard extends StatelessWidget { + const _SeriesCard({required this.item, required this.showTag}); + + final _DramaItem item; + final bool showTag; + + @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( + 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), + ), + ), + ), + 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), + ), + ), + ], + ), + ); + } +} + +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}); + + final _ActorInfo actor; + + @override + Widget build(BuildContext context) { + final List> stats = [ + [actor.bornYear, 'Born Year'], + [actor.height, 'Height'], + [actor.nationality, 'Nationality'], + ]; + return Container( + height: 68, + decoration: BoxDecoration( + color: const Color(0xFFF4F4F4), + border: Border.all(color: const Color(0xFFD8D8D8)), + ), + child: Row( + children: List.generate(stats.length, (index) { + return Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + NovyText( + stats[index][0], + color: Colors.black, + fontSize: 18, + fontFamily: ConstFont.montMedium, + ), + const SizedBox(height: 7), + NovyText( + stats[index][1], + color: const Color(0xFF777777), + fontSize: 13, + fontFamily: ConstFont.montRegular, + ), + ], + ), + ); + }), + ), + ); + } +} + +class _SectionHeader extends StatelessWidget { + const _SectionHeader({required this.title}); + + final String title; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: NovyText( + title, + color: Colors.black, + fontSize: 19, + fontFamily: ConstFont.montBlack, + ), + ); + } +} + +class _OutlinedTitle extends StatelessWidget { + const _OutlinedTitle(this.text, {required this.fontSize}); + + final String text; + final double fontSize; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Text( + text, + maxLines: 2, + style: TextStyle( + fontSize: fontSize, + height: 1.05, + fontFamily: ConstFont.montBlack, + foreground: Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 3.5 + ..color = Colors.black, + ), + ), + Text( + text, + maxLines: 2, + style: TextStyle( + color: Colors.white, + fontSize: fontSize, + height: 1.05, + fontFamily: ConstFont.montBlack, + ), + ), + ], + ); + } +} + +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(); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(7, (index) { + return Container( + width: index == 1 ? 26 : 7, + height: 7, + margin: const EdgeInsets.symmetric(horizontal: 3), + color: index == 1 ? 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 palette; +} + +class _ActorInfo { + const _ActorInfo({ + required this.name, + required this.tags, + required this.bornYear, + required this.height, + required this.nationality, + }); + + final String name; + final String tags; + final String bornYear; + final String height; + final String nationality; +} diff --git a/lib/pages/first/widgets/recent_history_view.dart b/lib/pages/first/widgets/recent_history_view.dart new file mode 100644 index 0000000..634dd06 --- /dev/null +++ b/lib/pages/first/widgets/recent_history_view.dart @@ -0,0 +1,90 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:novyronst/common/help/help_image.dart'; +import 'package:novyronst/tools/widgets/novy_com_widget.dart'; + + +class RecentHistoryView extends StatelessWidget { + const RecentHistoryView({super.key, + required this.searchHistory, + this.onTapHistory, + this.onTapClear, + }); + + final List searchHistory; + final ValueChanged? onTapHistory; + final Function()? onTapClear; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 15.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded(child: NovyText('Recent Searches', + fontSize: 14, + color: Colors.white, + fontWeight: FontWeight.bold, + lineHeight: 1, + )), + InkWell(onTap: () { + onTapClear?.call(); + }, child: HelpImage.asset(ConstImage.remove, width: 36, height: 36)), + ], + ), + const SizedBox(height: 8), + Wrap( + spacing: 16.w, + runSpacing: 14.h, + children: searchHistory + .map( + (item) => _HistoryChip( + text: item, + onTap: () => onTapHistory?.call(item), + ), + ) + .toList(), + ), + ], + ), + ); + } +} + + +class _HistoryChip extends StatelessWidget { + const _HistoryChip({ + required this.text, + this.onTap, + }); + + final String text; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.h), + decoration: BoxDecoration( + color: Colors.white.withAlpha(39), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + text, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.w500, + height: 1, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/first/widgets/search_view.dart b/lib/pages/first/widgets/search_view.dart index 35ea907..d0d5e03 100644 --- a/lib/pages/first/widgets/search_view.dart +++ b/lib/pages/first/widgets/search_view.dart @@ -1,9 +1,18 @@ 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, + }); + + final bool onlyTap; + final Function()? onTap; @override State createState() { @@ -39,7 +48,12 @@ class _SearchViewState extends State { children: [ Icon(Icons.search, color: Colors.white, size: 24), - Expanded(child: CupertinoTextField( + Expanded(child:widget.onlyTap + ? NovyText('What are...... ', + fontSize: 13, + color: Colors.white.withAlpha(127), + ) + : CupertinoTextField( controller: _searchController, // focusNode: widget.focusNode, decoration: BoxDecoration(), @@ -60,8 +74,7 @@ class _SearchViewState extends State { ),), HelpImage.asset(ConstImage.homeSearch, width: 78, height: 36, fit: BoxFit.fill) - - + ], ), ) @@ -71,8 +84,78 @@ class _SearchViewState extends State { ], ), ), + ).addInkWell(onTap: widget.onlyTap ? widget.onTap : null); + } +} + + + +/// 搜索页面的搜索框 +class SeSearchView extends StatefulWidget { + const SeSearchView({super.key, + this.onClickSearch, + }); + + final Function(String)? onClickSearch; + + @override + State createState() { + return _SeSearchViewState(); + } +} + +class _SeSearchViewState extends State { + + final _searchController = TextEditingController(); + + @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: _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: () { + if (_searchController.text.trim().isNotEmpty) { + widget.onClickSearch?.call(_searchController.text); + } + }, + ),), + + HelpImage.asset(ConstImage.homeSearch, + width: 78, height: 36, fit: BoxFit.fill + ).addInkWell(onTap: () { + if (_searchController.text.trim().isNotEmpty) { + widget.onClickSearch?.call(_searchController.text); + } + }) + + ], + ), + ) + ], + ) ); } - - } \ No newline at end of file diff --git a/lib/pages/my/pages/my_page.dart b/lib/pages/my/pages/my_page.dart index df8f61f..14b75df 100644 --- a/lib/pages/my/pages/my_page.dart +++ b/lib/pages/my/pages/my_page.dart @@ -1,4 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:novyronst/common/help/help_image.dart'; +import 'package:novyronst/pages/my/widgets/ny_dash_line.dart'; +import 'package:novyronst/root/lib_export.dart'; +import 'package:novyronst/tools/widgets/app_bg_page.dart'; +import 'package:novyronst/tools/widgets/novy_com_widget.dart'; class MyPage extends StatefulWidget { const MyPage({super.key}); @@ -13,6 +18,63 @@ class _RootPageState extends State { @override Widget build(BuildContext context) { - return Scaffold(); + return AppBgPage( + typeBg: 2, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + SizedBox(height: AppScreen.statusBarHeight + 48), + _buildUserInfo(), + NyDashLine() + ], + ), + ), + ); + } + + Widget _buildUserInfo() { + return Row( + children: [ + Container( + clipBehavior: Clip.antiAlias, + width: 60, height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withAlpha(127), width: 1) + ), + child: ClipOval( + child: HelpImage.asset(ConstImage.homeTopLogo, width: 60, height: 60, fit: BoxFit.fill), + ) + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NovyText('Visitor', + fontSize: 18, + ), + const SizedBox(height: 6), + Container( + height: 18, + padding: const EdgeInsets.symmetric(horizontal: 9), + decoration: BoxDecoration( + color: Colors.white.withAlpha(60), + borderRadius: BorderRadius.circular(9), + ), + child: Row( + children: [ + NovyText('ID: 3242545454', + fontSize: 11, + ), + const SizedBox(width: 6), + HelpImage.asset(ConstImage.copy, width: 12, height: 12) + ], + ), + ) + ], + ) + ], + ); } } \ No newline at end of file diff --git a/lib/pages/my/widgets/mine_func_view.dart b/lib/pages/my/widgets/mine_func_view.dart new file mode 100644 index 0000000..b184adf --- /dev/null +++ b/lib/pages/my/widgets/mine_func_view.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class MineFuncView extends StatelessWidget { + const MineFuncView({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} \ No newline at end of file diff --git a/lib/pages/my/widgets/ny_dash_line.dart b/lib/pages/my/widgets/ny_dash_line.dart new file mode 100644 index 0000000..8c193f9 --- /dev/null +++ b/lib/pages/my/widgets/ny_dash_line.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:novyronst/common/const/const_color.dart'; + + + +class NyDashLine extends StatelessWidget { + final double width; + final double height; + final double dashWidth; + final double dashSpace; + final Axis direction; + + const NyDashLine({ + super.key, + this.width = double.infinity, + this.height = 2, + this.dashWidth = 4, + this.dashSpace = 2, + this.direction = Axis.horizontal, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size(width, height), + painter: _DashedLinePainter( + color: ConstColor.withValue('#404040'), + dashWidth: dashWidth, + dashSpace: dashSpace, + direction: direction, + ), + ); + } +} + +class _DashedLinePainter extends CustomPainter { + final Color color; + final double dashWidth; + final double dashSpace; + final Axis direction; + + _DashedLinePainter({ + required this.color, + required this.dashWidth, + required this.dashSpace, + required this.direction, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = direction == Axis.horizontal ? size.height : size.width + ..style = PaintingStyle.stroke; + + if (direction == Axis.horizontal) { + double startX = 0; + while (startX < size.width) { + canvas.drawLine( + Offset(startX, size.height / 2), + Offset(startX + dashWidth, size.height / 2), + paint, + ); + startX += dashWidth + dashSpace; + } + } else { + double startY = 0; + while (startY < size.height) { + canvas.drawLine( + Offset(size.width / 2, startY), + Offset(size.width / 2, startY + dashWidth), + paint, + ); + startY += dashWidth + dashSpace; + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} \ No newline at end of file diff --git a/lib/tools/util/search_history_util.dart b/lib/tools/util/search_history_util.dart new file mode 100644 index 0000000..798661e --- /dev/null +++ b/lib/tools/util/search_history_util.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; + +import 'package:novyronst/tools/util/sp_tool.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// 本地搜索历史(SharedPreferences),key 与 [AppSp] 规则一致(`.search.history`)。 +class SearchHistoryUtil { + SearchHistoryUtil._(); + + static const String _storageKey = 'search.history'; + static const int _maxHistory = 20; + + static Future> _readList(SharedPreferences prefs) async { + final raw = prefs.getString(_storageKey); + if (raw == null || raw.isEmpty) return []; + try { + final decoded = jsonDecode(raw); + if (decoded is! List) return []; + return decoded.map((e) => e.toString()).where((s) => s.isNotEmpty).toList(); + } catch (_) { + return []; + } + } + + static Future _writeList(SharedPreferences prefs, List list) async { + await prefs.setString(_storageKey, jsonEncode(list)); + } + + /// 查询搜索历史列表(新记录在前)。 + static Future> getSearchList() async { + final prefs = await SpTool.getInstance(); + return _readList(prefs); + } + + /// 添加一条搜索;去重、去空白,并移到最前。 + static Future addSearch(String keyword) async { + final q = keyword.trim(); + if (q.isEmpty) return; + + final prefs = await SpTool.getInstance(); + final list = await _readList(prefs); + list.removeWhere((e) => e == q); + list.insert(0, q); + if (list.length > _maxHistory) { + list.removeRange(_maxHistory, list.length); + } + await _writeList(prefs, list); + } + + /// 删除单条搜索记录。 + static Future deleteSearch(String keyword) async { + final prefs = await SpTool.getInstance(); + final list = await _readList(prefs); + list.removeWhere((e) => e == keyword); + await _writeList(prefs, list); + } + + /// 清空全部搜索历史。 + static Future clearAllSearch() async { + final prefs = await SpTool.getInstance(); + await prefs.remove(_storageKey); + } +} diff --git a/lib/tools/util/sp_tool.dart b/lib/tools/util/sp_tool.dart new file mode 100644 index 0000000..332cfbe --- /dev/null +++ b/lib/tools/util/sp_tool.dart @@ -0,0 +1,57 @@ + +import 'package:shared_preferences/shared_preferences.dart'; + + +class SpTool { + static SharedPreferences? _instance; + static final String _basePrefix = "app."; + static Future? _instanceFuture; + + static Future getInstance() { + if (_instance != null) { + return Future.value(_instance); + } + + _instanceFuture ??= SharedPreferences.getInstance().then((prefs) { + _instance = prefs; + return prefs; + }); + + return _instanceFuture!; + } + + void setString(String key, String value) async { + final prefs = await getInstance(); + prefs.setString("$_basePrefix$key", value); + } + + Future getString(String key) async { + final prefs = await getInstance(); + return prefs.getString("$_basePrefix$key") ?? ''; + } + + void setBool(String key, bool value) async { + final prefs = await getInstance(); + prefs.setBool("$_basePrefix$key", value); + } + + Future getBool(String key) async { + final prefs = await getInstance(); + return prefs.getBool("$_basePrefix$key") ?? false; + } + + void setInt(String key, int value) async { + final prefs = await getInstance(); + prefs.setInt("$_basePrefix$key", value); + } + + Future getInt(String key) async { + final prefs = await getInstance(); + return prefs.getInt("$_basePrefix$key") ?? 0; + } + + void deleteKey(String key) async { + final prefs = await getInstance(); + prefs.remove("$_basePrefix$key"); + } +} diff --git a/lib/tools/widgets/app_bg_page.dart b/lib/tools/widgets/app_bg_page.dart index d6a3244..bc35594 100644 --- a/lib/tools/widgets/app_bg_page.dart +++ b/lib/tools/widgets/app_bg_page.dart @@ -6,10 +6,11 @@ class AppBgPage extends StatelessWidget { const AppBgPage({ super.key, this.child, - + this.typeBg = 1, }); final Widget? child; + final int typeBg; @override Widget build(BuildContext context) { @@ -19,7 +20,10 @@ class AppBgPage extends StatelessWidget { SizedBox( width: double.infinity, height: double.infinity, - child: HelpImage.asset(ConstImage.pageBg, fit: BoxFit.fill), + child: HelpImage.asset( + typeBg==1 ? ConstImage.pageBg : ConstImage.mineBg, + fit: BoxFit.fill + ), ), child ?? SizedBox() ], diff --git a/lib/tools/widgets/novy_app_bar.dart b/lib/tools/widgets/novy_app_bar.dart new file mode 100644 index 0000000..8c662a4 --- /dev/null +++ b/lib/tools/widgets/novy_app_bar.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:novyronst/common/help/app_nav.dart'; +import 'package:novyronst/tools/widgets/novy_com_widget.dart'; + +import '../../common/const/const_color.dart'; + + + +class NovyAppBar extends StatelessWidget implements PreferredSizeWidget { + const NovyAppBar({ + super.key, + this.title, + this.titleWidget, + this.leading, + this.actions, + this.onBack, + this.showBackButton = true, + this.centerTitle = true, + this.backgroundColor, + this.foregroundColor, + this.elevation = 0, + this.bottom, + this.toolbarHeight = kToolbarHeight, + this.titleColor, + }); + + final String? title; + final Color? titleColor; + final Widget? titleWidget; + final Widget? leading; + final List? actions; + final VoidCallback? onBack; + final bool showBackButton; + final bool centerTitle; + final Color? backgroundColor; + final Color? foregroundColor; + final double elevation; + final PreferredSizeWidget? bottom; + final double toolbarHeight; + + @override + Size get preferredSize => + Size.fromHeight(toolbarHeight + (bottom?.preferredSize.height ?? 0)); + + @override + Widget build(BuildContext context) { + + return _buildNormalAppBar(context); + } + + PreferredSizeWidget _buildNormalAppBar(BuildContext context) { + + final canPop = Navigator.of(context).canPop(); + + Widget? resolvedLeading = leading; + if (resolvedLeading == null && showBackButton && canPop) { + resolvedLeading = IconButton( + icon: Icon(Icons.arrow_back_ios, size: 18, color: Colors.white), + onPressed: onBack ?? () => AppNav.back(), + ); + } + + return AppBar( + title: titleWidget ?? (title != null + ? NovyText(title!, fontSize: 18, fontWeight: FontWeight.bold, color: titleColor) + : null), + leading: resolvedLeading, + actions: actions, + centerTitle: centerTitle, + backgroundColor: backgroundColor ?? ConstColor.withValue('#110D08'), + foregroundColor: foregroundColor, + elevation: elevation, + scrolledUnderElevation: 0, + toolbarHeight: toolbarHeight, + ); + } + + +} diff --git a/pubspec.yaml b/pubspec.yaml index bfea2dd..26dac61 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ flutter: assets: - assets/main/ - assets/imgs/ + - assets/other/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see