From a3426831ed168838c86713c7a2a77e1e59a4c037 Mon Sep 17 00:00:00 2001 From: phoenix--zhang <471155679@qq.com> Date: Tue, 7 Jul 2026 16:04:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=86=E9=A2=91=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E9=A1=B5=EF=BC=8C=E5=88=86=E7=B1=BB=E9=A1=B5=E3=80=82=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E4=B8=AD=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/imgs/home_head.png | Bin 0 -> 21714 bytes assets/other/app_logo.png | Bin 0 -> 52815 bytes lib/common/help/help_image.dart | 4 + .../collect/pages/collect_page_helpers.dart | 14 + lib/pages/collect/pages/collection_page.dart | 391 ++-------- lib/pages/collect/pages/favorite_page.dart | 194 +++++ lib/pages/collect/pages/history_page.dart | 179 +++++ lib/pages/first/pages/category_list_page.dart | 280 ++++++++ lib/pages/first/pages/first_page.dart | 205 +++++- .../player_detail/video_detail_page.dart | 291 ++++++++ .../first/player_detail/video_esp_page.dart | 217 ++++++ lib/pages/first/widgets/home_card_widget.dart | 99 +++ lib/pages/first/widgets/home_first_view.dart | 83 ++- lib/pages/first/widgets/home_rank_widget.dart | 173 ++--- .../widgets/home_remaining_sections.dart | 669 ++++++------------ lib/pages/first/widgets/search_view.dart | 238 ++++--- lib/pages/my/pages/about_page.dart | 115 +++ lib/pages/my/pages/mine_history_page.dart | 198 ++++++ lib/pages/my/pages/mine_link_page.dart | 298 ++++++++ lib/pages/my/pages/my_page.dart | 60 +- lib/pages/play/pages/play_page.dart | 44 +- lib/pages/play/widgets/player_view.dart | 58 +- lib/pages/widgets/ny_loading_widget.dart | 70 ++ lib/pages/widgets/ny_tag_widget.dart | 8 +- pubspec.lock | 64 ++ pubspec.yaml | 3 + 26 files changed, 2878 insertions(+), 1077 deletions(-) create mode 100644 assets/imgs/home_head.png create mode 100644 assets/other/app_logo.png create mode 100644 lib/pages/collect/pages/collect_page_helpers.dart create mode 100644 lib/pages/collect/pages/favorite_page.dart create mode 100644 lib/pages/collect/pages/history_page.dart create mode 100644 lib/pages/first/pages/category_list_page.dart create mode 100644 lib/pages/first/player_detail/video_detail_page.dart create mode 100644 lib/pages/first/player_detail/video_esp_page.dart create mode 100644 lib/pages/first/widgets/home_card_widget.dart create mode 100644 lib/pages/my/pages/about_page.dart create mode 100644 lib/pages/my/pages/mine_history_page.dart create mode 100644 lib/pages/my/pages/mine_link_page.dart create mode 100644 lib/pages/widgets/ny_loading_widget.dart diff --git a/assets/imgs/home_head.png b/assets/imgs/home_head.png new file mode 100644 index 0000000000000000000000000000000000000000..5bb6528d60c9a66b5ea8cc9756267fa41fcdfebc GIT binary patch literal 21714 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 literal 0 HcmV?d00001 diff --git a/assets/other/app_logo.png b/assets/other/app_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7759ae2581bb349143ffddf97c08acd203dcd25c GIT binary patch literal 52815 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≻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 { - 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, - ), - ]; + final PageController _pageController = PageController(); + int _currentIndex = 0; + bool _isEditing = false; - 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, - ), - ]; + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } - bool _showHistory = false; + void _changePage(int index) { + setState(() { + _currentIndex = index; + _isEditing = false; + }); + _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 220), + curve: Curves.easeOut, + ); + } @override Widget build(BuildContext context) { @@ -142,11 +46,18 @@ class _RootPageState extends State { children: [ _buildHeader(), Expanded( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 180), - child: _showHistory - ? _buildHistoryList() - : _buildFavoritesGrid(), + child: PageView( + controller: _pageController, + onPageChanged: (index) { + setState(() { + _currentIndex = index; + _isEditing = false; + }); + }, + children: [ + FavoritePage(isEditing: _isEditing && _currentIndex == 0), + HistoryPage(isEditing: _isEditing && _currentIndex == 1), + ], ), ), ], @@ -157,49 +68,41 @@ class _RootPageState extends State { Widget _buildHeader() { return Padding( - padding: const EdgeInsets.fromLTRB(30, 18, 26, 12), + padding: const EdgeInsets.fromLTRB(16, 10, 16, 12), child: Row( children: [ - _buildTab( - 'Favorites', - active: !_showHistory, - onTap: () { - setState(() { - _showHistory = false; - }); - }, - ), + _buildTab(0), const SizedBox(width: 16), - _buildTab( - 'History', - active: _showHistory, - onTap: () { + _buildTab(1), + const Spacer(), + IconButton( + onPressed: () { setState(() { - _showHistory = true; + _isEditing = !_isEditing; }); }, + icon: Icon( + _isEditing ? Icons.done : Icons.edit_outlined, + color: Colors.white, + size: 22, + ), ), - const Spacer(), - const Icon(Icons.edit_outlined, color: Colors.white, size: 22), ], ), ); } - Widget _buildTab( - String title, { - required bool active, - required VoidCallback onTap, - }) { + Widget _buildTab(int index) { + final active = _currentIndex == index; return GestureDetector( behavior: HitTestBehavior.opaque, - onTap: onTap, + onTap: () => _changePage(index), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - title, + collectionTabLabel(index), style: TextStyle( color: active ? Colors.white @@ -223,196 +126,4 @@ class _RootPageState extends State { ), ); } - - 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'; } diff --git a/lib/pages/collect/pages/favorite_page.dart b/lib/pages/collect/pages/favorite_page.dart new file mode 100644 index 0000000..cc8eae8 --- /dev/null +++ b/lib/pages/collect/pages/favorite_page.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:novyronst/common/api/video_request.dart'; +import 'package:novyronst/common/model/ny_drama_model.dart'; +import 'package:novyronst/pages/collect/pages/collect_page_helpers.dart'; +import 'package:novyronst/pages/widgets/ny_empty_view.dart'; + +class FavoritePage extends StatefulWidget { + const FavoritePage({ + super.key, + required this.isEditing, + this.fetchCollections, + this.deleteCollection, + }); + + final bool isEditing; + final Future> Function({int page, int pageSize})? + fetchCollections; + final Future Function({required int shortPlayId})? deleteCollection; + + @override + State createState() => _FavoritePageState(); +} + +class _FavoritePageState extends State { + final List _items = []; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _loadFavorites(); + } + + Future _loadFavorites() async { + setState(() { + _isLoading = true; + }); + + final fetch = widget.fetchCollections ?? VideoRequest.getCollections; + final items = await fetch(page: 1, pageSize: 20); + if (!mounted) return; + + setState(() { + _items + ..clear() + ..addAll(items); + _isLoading = false; + }); + } + + Future _deleteFavorite(int index) async { + if (index < 0 || index >= _items.length) return; + + final item = _items.removeAt(index); + setState(() {}); + + final delete = widget.deleteCollection ?? VideoRequest.cancelCollect; + final success = await delete(shortPlayId: collectShortPlayId(item)); + if (!mounted || success) return; + + setState(() { + _items.insert(index.clamp(0, _items.length), item); + }); + } + + @override + Widget build(BuildContext context) { + if (_isLoading && _items.isEmpty) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + if (_items.isEmpty) { + return const NyEmptyView(); + } + + return GridView.builder( + padding: const EdgeInsets.fromLTRB(16, 4, 14, 96), + itemCount: _items.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 8, + mainAxisSpacing: 14, + childAspectRatio: 109 / 215, + ), + itemBuilder: (context, index) { + return _FavoriteCard( + item: _items[index], + isEditing: widget.isEditing, + onDelete: () => _deleteFavorite(index), + ); + }, + ); + } +} + +class _FavoriteCard extends StatelessWidget { + const _FavoriteCard({ + required this.item, + required this.isEditing, + required this.onDelete, + }); + + final NyDramaModel item; + final bool isEditing; + final VoidCallback onDelete; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + clipBehavior: Clip.none, + 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), + ); + }, + ), + ), + if (isEditing) + Positioned( + top: 4, + right: 4, + child: _DeleteButton(onTap: onDelete), + ), + ], + ), + const SizedBox(height: 8), + Text( + item.name, + 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( + dramaEpisodeText(item), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.48), + fontSize: 11, + fontFamily: 'Montserrat-Medium', + fontWeight: FontWeight.w500, + ), + ), + ], + ); + }, + ); + } +} + +class _DeleteButton extends StatelessWidget { + const _DeleteButton({required this.onTap}); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.black.withValues(alpha: 0.62), + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: onTap, + child: const SizedBox( + width: 30, + height: 30, + child: Icon(Icons.delete_outline, color: Colors.redAccent, size: 18), + ), + ), + ); + } +} diff --git a/lib/pages/collect/pages/history_page.dart b/lib/pages/collect/pages/history_page.dart new file mode 100644 index 0000000..6d60e72 --- /dev/null +++ b/lib/pages/collect/pages/history_page.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:novyronst/common/api/video_request.dart'; +import 'package:novyronst/common/model/ny_drama_model.dart'; +import 'package:novyronst/pages/collect/pages/collect_page_helpers.dart'; + +class HistoryPage extends StatefulWidget { + const HistoryPage({super.key, required this.isEditing, this.fetchHistory}); + + final bool isEditing; + final Future> Function({int page, int pageSize})? + fetchHistory; + + @override + State createState() => _HistoryPageState(); +} + +class _HistoryPageState extends State { + final List _items = []; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _loadHistory(); + } + + Future _loadHistory() async { + setState(() { + _isLoading = true; + }); + + final fetch = widget.fetchHistory ?? VideoRequest.getHistory; + final items = await fetch(page: 1, pageSize: 20); + if (!mounted) return; + + setState(() { + _items + ..clear() + ..addAll(items); + _isLoading = false; + }); + } + + void _deleteHistory(int index) { + if (index < 0 || index >= _items.length) return; + setState(() { + _items.removeAt(index); + }); + } + + @override + Widget build(BuildContext context) { + if (_isLoading && _items.isEmpty) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + if (_items.isEmpty) { + return const Center( + child: Text('No history', style: TextStyle(color: Colors.white70)), + ); + } + + return ListView.separated( + padding: const EdgeInsets.fromLTRB(30, 8, 20, 96), + itemCount: _items.length, + separatorBuilder: (context, index) => const SizedBox(height: 13), + itemBuilder: (context, index) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 30, + child: _HistoryLeadingButton( + isEditing: widget.isEditing, + onDelete: () => _deleteHistory(index), + ), + ), + const SizedBox(width: 18), + Expanded(child: _HistoryItem(item: _items[index])), + ], + ); + }, + ); + } +} + +class _HistoryLeadingButton extends StatelessWidget { + const _HistoryLeadingButton({ + required this.isEditing, + required this.onDelete, + }); + + final bool isEditing; + final VoidCallback onDelete; + + @override + Widget build(BuildContext context) { + if (isEditing) { + return IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor(width: 30, height: 40), + onPressed: onDelete, + icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 28), + ); + } + + return Icon( + Icons.bookmark_border, + color: Colors.white.withValues(alpha: 0.28), + size: 32, + ); + } +} + +class _HistoryItem extends StatelessWidget { + const _HistoryItem({required this.item}); + + final NyDramaModel 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.name, + 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( + dramaEpisodeText(item), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.45), + fontSize: 12, + fontFamily: 'Montserrat-Medium', + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/first/pages/category_list_page.dart b/lib/pages/first/pages/category_list_page.dart new file mode 100644 index 0000000..45a4d90 --- /dev/null +++ b/lib/pages/first/pages/category_list_page.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.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/help/help_time.dart'; +import 'package:novyronst/common/model/ny_category_model.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_empty_view.dart'; +import 'package:novyronst/pages/widgets/ny_tag_widget.dart'; +import 'package:novyronst/tools/widgets/novy_app_bar.dart'; +import 'package:novyronst/tools/widgets/novy_com_widget.dart'; + +class CategoryListPage extends StatefulWidget { + const CategoryListPage({super.key, this.preCategory}); + + final NyCategoryModel? preCategory; + + @override + State createState() => _CatePageState(); +} + +class _CatePageState extends State { + final List _categories = []; + final List _dramas = []; + int _selectedIndex = 0; + int? _loadingCategoryId; + bool _loadingCategories = true; + bool _loadingDramas = false; + + @override + void initState() { + super.initState(); + _loadCategories(); + } + + Future _loadCategories() async { + final categories = await VideoRequest.getCategoriesList(); + if (!mounted) return; + + final nextIndex = selectedCategoryIndex(categories, widget.preCategory); + setState(() { + _categories + ..clear() + ..addAll(categories); + _selectedIndex = nextIndex < 0 ? 0 : nextIndex; + _loadingCategories = false; + }); + + if (categories.isNotEmpty) { + _loadCategoryDetail(categories[_selectedIndex].id); + } + } + + Future _loadCategoryDetail(int categoryId) async { + _loadingCategoryId = categoryId; + setState(() { + _loadingDramas = true; + _dramas.clear(); + }); + + final dramas = await VideoRequest.getCategoriesDetail( + categoryId: categoryId, + ); + if (!mounted || _loadingCategoryId != categoryId) return; + + setState(() { + _dramas + ..clear() + ..addAll(dramas); + _loadingDramas = false; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: const NovyAppBar(title: 'Categories', titleColor: Colors.white), + body: SafeArea( + top: false, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCategoryList(), + Expanded(child: _buildDramaList()), + ], + ), + ), + ); + } + + Widget _buildCategoryList() { + return Container( + width: 112.w, + height: double.infinity, + padding: EdgeInsets.only(top: 12.h, bottom: 18.h), + color: Colors.black, + child: _loadingCategories + ? const SizedBox.shrink() + : ListView.builder( + padding: EdgeInsets.zero, + itemCount: _categories.length, + itemBuilder: (context, index) => _buildCategoryItem(index), + ), + ); + } + + Widget _buildCategoryItem(int index) { + final category = _categories[index]; + final selected = index == _selectedIndex; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (selected) return; + setState(() { + _selectedIndex = index; + }); + _loadCategoryDetail(category.id); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 160), + curve: Curves.easeOut, + height: 58, + margin: EdgeInsets.only(left: 8.w, right: 8.w, bottom: 6.h), + padding: EdgeInsets.symmetric(horizontal: 8.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: selected ? ConstColor.btGreen : const Color(0xFF151515), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: selected + ? Colors.black + : Colors.white.withValues(alpha: 0.08), + width: 1, + ), + ), + child: NovyText( + category.name, + color: selected ? Colors.black : Colors.white.withValues(alpha: 0.62), + fontSize: 12, + fontFamily: selected ? ConstFont.montBold : ConstFont.montMedium, + textAlign: TextAlign.center, + maxLines: 2, + lineHeight: 1.05, + ), + ), + ); + } + + Widget _buildDramaList() { + if (_loadingDramas) { + return Center( + child: CircularProgressIndicator(color: ConstColor.btGreen), + ); + } + + if (_dramas.isEmpty) { + return const NyEmptyView(); + } + + return ListView.separated( + padding: EdgeInsets.only(left: 4.w, right: 16.w, top: 12.h, bottom: 28.h), + itemCount: _dramas.length, + separatorBuilder: (context, index) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return _DramaItem(drama: _dramas[index]); + }, + ); + } +} + +class _DramaItem extends StatelessWidget { + const _DramaItem({required this.drama}); + + final NyDramaModel drama; + + @override + Widget build(BuildContext context) { + final tag = categoryDramaTag(drama); + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => AppNav.to(VideoDetailPage(drama: drama)), + child: Container( + height: 118.h, + padding: EdgeInsets.all(8.w), + decoration: BoxDecoration( + color: const Color(0xFF101010), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.white.withValues(alpha: 0.08)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HelpImage.networkRadius( + drama.imageUrl, + width: 78.w, + height: 102.h, + fit: BoxFit.cover, + borderRadius: BorderRadius.circular(4), + ), + SizedBox(width: 10.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NovyText( + drama.name, + color: Colors.white, + fontSize: 14, + fontFamily: ConstFont.montBold, + maxLines: 2, + lineHeight: 1.08, + ), + SizedBox(height: 6.h), + NovyText( + drama.description, + color: Colors.white.withValues(alpha: 0.56), + fontSize: 11, + maxLines: 2, + lineHeight: 1.2, + ), + const Spacer(), + Row( + children: [ + if (tag.isNotEmpty) ...[ + NyTagWidget( + text: tag, + bgColor: Colors.white.withValues(alpha: 0.12), + textColor: Colors.white.withValues(alpha: 0.72), + ), + SizedBox(width: 10.w), + ], + NyHotWidget( + text: HelpTime.formatNum(drama.watchTotal), + textColor: ConstColor.btGreen, + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +int selectedCategoryIndex( + List categories, + NyCategoryModel? preCategory, +) { + if (categories.isEmpty) return -1; + if (preCategory == null) return 0; + + final index = categories.indexWhere( + (category) => category.id == preCategory.id, + ); + return index >= 0 ? index : 0; +} + +String categoryDramaTag(NyDramaModel drama) { + final category = drama.category; + if (category != null && category.isNotEmpty) { + return category.first; + } + + final categoryList = drama.categoryList; + if (categoryList != null && categoryList.isNotEmpty) { + return categoryList.first.name; + } + + return drama.tagType; +} diff --git a/lib/pages/first/pages/first_page.dart b/lib/pages/first/pages/first_page.dart index 8d831bd..8b39431 100644 --- a/lib/pages/first/pages/first_page.dart +++ b/lib/pages/first/pages/first_page.dart @@ -1,4 +1,5 @@ -import 'package:easy_refresh/easy_refresh.dart'; +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:novyronst/common/api/video_request.dart'; import 'package:novyronst/common/help/app_nav.dart'; @@ -6,6 +7,7 @@ import 'package:novyronst/common/model/ny_category_model.dart'; import 'package:novyronst/common/model/ny_drama_model.dart'; import 'package:novyronst/common/model/ny_home_model.dart'; import 'package:novyronst/pages/first/pages/search_page.dart'; +import 'package:novyronst/pages/first/widgets/home_card_widget.dart'; import 'package:novyronst/pages/first/widgets/home_first_view.dart'; import 'package:novyronst/pages/first/widgets/home_rank_widget.dart'; import 'package:novyronst/pages/first/widgets/home_remaining_sections.dart'; @@ -24,12 +26,24 @@ class FirstPage extends StatefulWidget { } class _RootPageState extends State { - List _categories = []; List _homeDatas = []; - final EasyRefreshController _refreshController = EasyRefreshController( - controlFinishRefresh: true, - ); + final List _cateTitles = [ + 'Counterattack Life', + 'Reborn and Returned', + 'God of War Returns', + 'System Flow', + ]; + + final List _cateTags = [ + 'home_v3_recommand', + 'new_recommand', + 'get_details_recommand', + 'week_ranking', + ]; + + NyDramaModel? _dramaModel; + bool _useGlassHeader = false; @override void initState() { @@ -45,8 +59,17 @@ class _RootPageState extends State { _homeDatas = value; }); - for (NyHomeModel element in value) { - print('ele len===: ${element.moduleKey} : ${element.dataList.length}'); + List temp = []; + temp = _homeDatas + .firstWhere((element) => element.moduleKey == 'week_ranking') + .dataList; + if (temp.isEmpty) { + temp = _homeDatas + .firstWhere((element) => element.moduleKey == 'home_v3_recommand') + .dataList; + } + if (temp.isNotEmpty) { + _dramaModel = temp.first; } }); } @@ -56,48 +79,158 @@ class _RootPageState extends State { setState(() { _categories = value; }); + // for (NyCategoryModel element in value) { + // print('NyCategoryModel===: ${element.name}'); + // } }); } @override Widget build(BuildContext context) { return AppBgPage( - child: Column( + child: Stack( children: [ - HomeSearchView( - onlyTap: true, - onTap: () { - AppNav.to(SearchPage()); - }, - ), - _homeDatas.isEmpty ? SizedBox() : Expanded( - child: CustomScrollView( - slivers: [ - SliverToBoxAdapter(child: HomeFirstView( - categories: _categories, - )), - SliverPadding( - padding: const EdgeInsets.only(left: 16, right: 10, top: 16, bottom: 8), - sliver: SliverToBoxAdapter( - child: HelpImage.asset(ConstImage.homeRank), - ) - ), - SliverPadding( - padding: const EdgeInsetsGeometry.symmetric(horizontal: 16), - sliver: SliverToBoxAdapter( - child: HomeRankWidget( - dramaRanks: _homeDatas - .firstWhere((element) => element.moduleKey == 'week_ranking') - .dataList, + Positioned.fill( + child: _homeDatas.isEmpty + ? const SizedBox() + : NotificationListener( + onNotification: _handleScrollNotification, + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: SizedBox(height: AppScreen.navBarHeight), + ), + SliverToBoxAdapter( + child: HomeFirstView(categories: _categories), + ), + SliverPadding( + padding: const EdgeInsets.only( + left: 16, + right: 10, + top: 16, + bottom: 8, + ), + sliver: SliverToBoxAdapter( + child: HelpImage.asset(ConstImage.homeRank), + ), + ), + SliverPadding( + padding: const EdgeInsetsGeometry.symmetric( + horizontal: 16, + ), + sliver: SliverToBoxAdapter( + child: HomeRankWidget( + dramaRanks: _homeDatas + .firstWhere( + (element) => + element.moduleKey == 'week_ranking', + ) + .dataList, + ), + ), + ), + SliverToBoxAdapter( + child: HomeRemainingSections(dramaOne: _dramaModel!), + ), + + ..._buildSliversFromData(), + + const SliverToBoxAdapter(child: SizedBox(height: 110)), + ], ), ), - ), - const SliverToBoxAdapter(child: HomeRemainingSections()), - ], + ), + Positioned( + left: 0, + right: 0, + top: 0, + child: _HomeGlassHeader( + useGlass: _useGlassHeader, + child: HomeSearchView( + onlyTap: true, + showLogo: !_useGlassHeader, + onTap: () { + AppNav.to(SearchPage()); + }, + ), ), ), ], ), ); } + + bool _handleScrollNotification(ScrollNotification notification) { + if (notification.metrics.axis != Axis.vertical) return false; + + final nextUseGlass = shouldUseHomeGlassHeader(notification.metrics.pixels); + if (nextUseGlass != _useGlassHeader) { + setState(() { + _useGlassHeader = nextUseGlass; + }); + } + + return false; + } + + List _buildSliversFromData() { + final List slivers = []; + + int index = 0; + for (final str in _cateTags) { + List list = _homeDatas + .firstWhere((element) => element.moduleKey == str) + .dataList; + if (list.isEmpty) continue; + slivers.add( + SliverPadding( + padding: const EdgeInsetsGeometry.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: HomeCardWidget( + title: _cateTitles[index], + dramaList: _homeDatas + .firstWhere((element) => element.moduleKey == str) + .dataList, + ), + ), + ), + ); + slivers.add(const SliverToBoxAdapter(child: SizedBox(height: 16))); + + index++; + } + + return slivers; + } +} + +class _HomeGlassHeader extends StatelessWidget { + const _HomeGlassHeader({required this.useGlass, required this.child}); + + final bool useGlass; + final Widget child; + + @override + Widget build(BuildContext context) { + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: useGlass ? 18 : 0, + sigmaY: useGlass ? 18 : 0, + ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOut, + color: useGlass + ? Colors.white.withValues(alpha: 0.16) + : Colors.transparent, + child: child, + ), + ), + ); + } +} + +bool shouldUseHomeGlassHeader(double scrollOffset) { + return scrollOffset > 8; } diff --git a/lib/pages/first/player_detail/video_detail_page.dart b/lib/pages/first/player_detail/video_detail_page.dart new file mode 100644 index 0000000..e516b0d --- /dev/null +++ b/lib/pages/first/player_detail/video_detail_page.dart @@ -0,0 +1,291 @@ +import 'package:flutter/material.dart'; +import 'package:novyronst/common/api/video_request.dart'; +import 'package:novyronst/common/model/ny_detail_model.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_esp_page.dart'; +import 'package:novyronst/pages/play/widgets/player_view.dart'; + +class VideoDetailPage extends StatefulWidget { + const VideoDetailPage({super.key, required this.drama}); + + final NyDramaModel drama; + + @override + State createState() => _VideoDetailPageState(); +} + +class _VideoDetailPageState extends State { + NyDetailModel? _detail; + List _videos = []; + int _currentIndex = 0; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _loadDetail(); + } + + Future _loadDetail() async { + setState(() { + _isLoading = true; + }); + + final detail = await VideoRequest.getVideoDetail( + shortPlayId: widget.drama.shortPlayId ?? widget.drama.shortId, + shortPlayVideoId: + widget.drama.shortPlayVideoId ?? + widget.drama.id ?? + widget.drama.shortId, + ); + if (!mounted) return; + + final videos = detail == null + ? [widget.drama] + : buildEpisodeDramaList(detail); + final nextIndex = detail == null + ? 0 + : episodeIndexForDrama(detail.episodeList, widget.drama); + + setState(() { + _detail = detail; + _videos = videos; + _currentIndex = nextIndex < 0 ? 0 : nextIndex; + _isLoading = false; + }); + } + + Future _toggleCollect(int index) async { + if (index < 0 || index >= _videos.length) return; + + final item = _videos[index]; + final videoId = item.shortPlayVideoId ?? item.id; + if (videoId == null) return; + + final shortPlayId = item.shortPlayId ?? item.shortId; + final oldIsCollect = item.isCollect ?? false; + final oldCollectTotal = item.collectTotal; + final newIsCollect = !oldIsCollect; + final newCollectTotal = oldCollectTotal + (newIsCollect ? 1 : -1); + + setState(() { + _videos[index] = item.copyWith( + isCollect: newIsCollect, + collectTotal: newCollectTotal < 0 ? 0 : newCollectTotal, + ); + }); + + final success = newIsCollect + ? await VideoRequest.collect(shortPlayId: shortPlayId, videoId: videoId) + : await VideoRequest.cancelCollect(shortPlayId: shortPlayId); + + if (!mounted || success) return; + + setState(() { + _videos[index] = _videos[index].copyWith( + isCollect: oldIsCollect, + collectTotal: oldCollectTotal, + ); + }); + } + + void _showEpisodeSheet() { + final detail = _detail; + if (detail == null || detail.episodeList.isEmpty) return; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return VideoEspPage( + detail: detail, + currentEpisode: _currentEpisode, + onSelectedEpisode: (episode) { + Navigator.pop(context); + final nextIndex = episodeIndexForEpisode( + detail.episodeList, + episode, + ); + if (nextIndex < 0) return; + setState(() { + _currentIndex = nextIndex; + }); + }, + ); + }, + ); + } + + int get _currentEpisode { + if (_videos.isEmpty || _currentIndex >= _videos.length) return 1; + return _videos[_currentIndex].episode ?? _currentIndex + 1; + } + + int get _totalEpisode { + final detail = _detail; + final total = + detail?.shortPlayInfo?.episodeTotal ?? widget.drama.episodeTotal; + if (total > 0) return total; + return detail?.episodeList.length ?? _videos.length; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + Positioned.fill(child: _buildBody()), + Positioned( + left: 10, + top: MediaQuery.paddingOf(context).top + 8, + child: IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white), + ), + ), + ], + ), + ); + } + + Widget _buildBody() { + if (_isLoading && _videos.isEmpty) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + if (_videos.isEmpty) { + return const Center( + child: Text('No videos', style: TextStyle(color: Colors.white70)), + ); + } + + return PlayerView( + videos: _videos, + initialIndex: _currentIndex, + bottomMetaSpacing: 24, + onPageChanged: (index) { + setState(() { + _currentIndex = index; + }); + }, + onCollectTap: _toggleCollect, + progressHeaderBuilder: (context, drama, index) { + return _EpisodeSelectorBar( + currentEpisode: drama.episode ?? index + 1, + totalEpisode: _totalEpisode, + onTap: _showEpisodeSheet, + ); + }, + ); + } +} + +class _EpisodeSelectorBar extends StatelessWidget { + const _EpisodeSelectorBar({ + required this.currentEpisode, + required this.totalEpisode, + required this.onTap, + }); + + final int currentEpisode; + final int totalEpisode; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Container( + height: 38, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + const Icon( + Icons.video_library_outlined, + color: Colors.white, + size: 17, + ), + const SizedBox(width: 10), + Text( + 'EP.$currentEpisode', + style: const TextStyle(color: Colors.white, fontSize: 13), + ), + Text( + ' / EP.$totalEpisode', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + fontSize: 13, + ), + ), + const Spacer(), + const Icon(Icons.arrow_forward_ios, color: Colors.white, size: 13), + ], + ), + ), + ); + } +} + +List buildEpisodeDramaList(NyDetailModel detail) { + final drama = detail.shortPlayInfo ?? detail.videoInfo; + final info = detail.videoInfo ?? detail.shortPlayInfo; + final total = drama?.episodeTotal ?? detail.episodeList.length; + + return detail.episodeList.map((episode) { + return NyDramaModel( + id: episode.id, + shortId: episode.shortId, + shortPlayId: episode.shortPlayId, + name: drama?.name ?? info?.name ?? '', + description: drama?.description ?? info?.description ?? '', + imageUrl: drama?.imageUrl ?? info?.imageUrl ?? '', + horizontallyImg: drama?.horizontallyImg ?? info?.horizontallyImg ?? '', + collectTotal: drama?.collectTotal ?? info?.collectTotal ?? 0, + watchTotal: drama?.watchTotal ?? info?.watchTotal ?? 0, + episodeTotal: total, + isCollect: detail.isCollect ?? drama?.isCollect ?? info?.isCollect, + videoUrl: episode.videoUrl, + videoInfo: {'video_url': episode.videoUrl}, + tagType: drama?.tagType ?? info?.tagType ?? '', + episode: episode.episode, + shortPlayVideoId: episode.shortPlayVideoId, + currentEpisode: episode.episode, + ); + }).toList(); +} + +int episodeIndexForEpisode( + List episodes, + NyEpisodeModel selectedEpisode, +) { + return episodes.indexWhere( + (episode) => episode.shortPlayVideoId == selectedEpisode.shortPlayVideoId, + ); +} + +int episodeIndexForDrama(List episodes, NyDramaModel drama) { + final videoId = drama.shortPlayVideoId ?? drama.id; + if (videoId != null) { + final index = episodes.indexWhere( + (episode) => episode.shortPlayVideoId == videoId, + ); + if (index >= 0) return index; + } + + final episodeNumber = drama.episode ?? drama.currentEpisode; + if (episodeNumber != null) { + return episodes.indexWhere((episode) => episode.episode == episodeNumber); + } + + return -1; +} diff --git a/lib/pages/first/player_detail/video_esp_page.dart b/lib/pages/first/player_detail/video_esp_page.dart new file mode 100644 index 0000000..423428f --- /dev/null +++ b/lib/pages/first/player_detail/video_esp_page.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:novyronst/common/const/const_color.dart'; +import 'package:novyronst/common/help/help_image.dart'; +import 'package:novyronst/common/model/ny_detail_model.dart'; +import 'package:novyronst/common/model/ny_drama_model.dart'; +import 'package:novyronst/common/model/ny_episode_model.dart'; +import 'package:novyronst/tools/util/app_screen.dart'; +import 'package:novyronst/tools/widgets/novy_com_widget.dart'; + +class VideoEspPage extends StatefulWidget { + const VideoEspPage({ + super.key, + required this.detail, + this.currentEpisode, + this.onSelectedEpisode, + }); + + final NyDetailModel detail; + final int? currentEpisode; + final ValueChanged? onSelectedEpisode; + + @override + State createState() => _VideoEspPageState(); +} + +class _VideoEspPageState extends State { + late int _selectedEpisode; + + NyDramaModel? get _drama => + widget.detail.shortPlayInfo ?? widget.detail.videoInfo; + + NyDramaModel? get _info => widget.detail.videoInfo; + + List get _episodes => widget.detail.episodeList; + + @override + void initState() { + super.initState(); + _selectedEpisode = + widget.currentEpisode ?? _drama?.currentEpisode ?? _drama?.episode ?? 1; + } + + @override + Widget build(BuildContext context) { + const height = 520.0; + return Container( + width: AppScreen.screenWidth, + height: height, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(18)), + ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(22, 24, 22, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDramaInfo(), + const SizedBox(height: 12), + Divider( + height: 1, + thickness: 0.5, + color: Colors.black.withValues(alpha: 0.08), + ), + const SizedBox(height: 12), + NovyText( + '$_episodeTotal EP', + color: Colors.black, + fontSize: 15, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 8), + Expanded(child: _buildEpisodeGrid()), + ], + ), + ), + ), + ); + } + + Widget _buildDramaInfo() { + final drama = _drama; + final imageUrl = drama?.imageUrl ?? ''; + + return SizedBox( + height: 130, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HelpImage.networkRadius( + imageUrl, + width: 96, + height: 130, + fit: BoxFit.cover, + borderRadius: BorderRadius.circular(2), + ), + const SizedBox(width: 16), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 7), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NovyText( + drama?.name ?? '', + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.w700, + maxLines: 2, + lineHeight: 1.12, + ), + const SizedBox(height: 10), + NovyText( + drama?.description ?? _info?.description ?? '', + color: Colors.black.withValues(alpha: 0.46), + fontSize: 14, + maxLines: 2, + lineHeight: 1.45, + ), + const SizedBox(height: 13), + _buildTag(), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildTag() { + final categoryList = _drama?.categoryList; + final name = categoryList?.isNotEmpty == true + ? categoryList!.first.name + : (_drama?.tagType ?? ''); + + if (name.isEmpty) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFFE7FFE5), + border: Border.all(color: const Color(0xFF86D57A), width: 1), + ), + child: NovyText( + name, + color: Colors.black, + fontSize: 14, + overflow: TextOverflow.visible, + ), + ); + } + + Widget _buildEpisodeGrid() { + if (_episodes.isEmpty) return const SizedBox.shrink(); + + return GridView.builder( + padding: EdgeInsets.zero, + physics: const BouncingScrollPhysics(), + itemCount: _episodes.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 61 / 50, + ), + itemBuilder: (context, index) { + return _buildEpisodeItem(_episodes[index]); + }, + ); + } + + Widget _buildEpisodeItem(NyEpisodeModel episode) { + final selected = episode.episode == _selectedEpisode; + final selectedPlayable = selected && !episode.isLock; + final selectedLocked = selected && episode.isLock; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (_selectedEpisode == episode.episode) return; + setState(() { + _selectedEpisode = episode.episode; + }); + widget.onSelectedEpisode?.call(episode); + }, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: selectedPlayable + ? ConstColor.btGreen + : const Color(0xFFF8F8F8), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: selectedPlayable + ? Colors.black + : (selectedLocked ? ConstColor.btGreen : Colors.transparent), + width: selected ? 1 : 0, + ), + ), + child: NovyText( + episode.episode.toString(), + color: Colors.black.withValues(alpha: 0.86), + fontSize: 20, + fontWeight: selected ? FontWeight.w700 : FontWeight.w400, + ), + ), + ); + } + + int get _episodeTotal { + final total = _drama?.episodeTotal ?? 0; + return total > 0 ? total : _episodes.length; + } +} diff --git a/lib/pages/first/widgets/home_card_widget.dart b/lib/pages/first/widgets/home_card_widget.dart new file mode 100644 index 0000000..c55ad58 --- /dev/null +++ b/lib/pages/first/widgets/home_card_widget.dart @@ -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 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), + ); + }), + ); + } +} diff --git a/lib/pages/first/widgets/home_first_view.dart b/lib/pages/first/widgets/home_first_view.dart index 525178e..0dbf08b 100644 --- a/lib/pages/first/widgets/home_first_view.dart +++ b/lib/pages/first/widgets/home_first_view.dart @@ -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 categories; @@ -24,21 +24,22 @@ class HomeFirstView extends StatefulWidget { } class _HomeFirstViewState extends State { - - 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 { 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 { 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 { 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 { 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 { 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), ], ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/first/widgets/home_rank_widget.dart b/lib/pages/first/widgets/home_rank_widget.dart index 5fe22b4..72a67c0 100644 --- a/lib/pages/first/widgets/home_rank_widget.dart +++ b/lib/pages/first/widgets/home_rank_widget.dart @@ -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 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 { ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/first/widgets/home_remaining_sections.dart b/lib/pages/first/widgets/home_remaining_sections.dart index 69fcfbc..d504725 100644 --- a/lib/pages/first/widgets/home_remaining_sections.dart +++ b/lib/pages/first/widgets/home_remaining_sections.dart @@ -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 createState() { + return _HomeRemainingSectionsState(); + } +} + +class _HomeRemainingSectionsState extends State { + 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 _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 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 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 episodeSeriesPageItems( + List 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); +} diff --git a/lib/pages/first/widgets/search_view.dart b/lib/pages/first/widgets/search_view.dart index 1b3cdac..67a46de 100644 --- a/lib/pages/first/widgets/search_view.dart +++ b/lib/pages/first/widgets/search_view.dart @@ -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 createState() { @@ -21,66 +23,92 @@ class HomeSearchView extends StatefulWidget { } class _SearchViewState extends State { - 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 { } } - - /// 搜索页面的搜索框 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 { - - @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); + } + }, + ), + ), + ], + ), + ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/my/pages/about_page.dart b/lib/pages/my/pages/about_page.dart new file mode 100644 index 0000000..04e81bd --- /dev/null +++ b/lib/pages/my/pages/about_page.dart @@ -0,0 +1,115 @@ + +import 'package:flutter/material.dart'; +import 'package:novyronst/common/api/api_string.dart'; +import 'package:novyronst/common/help/app_nav.dart'; +import 'package:novyronst/pages/my/pages/mine_link_page.dart'; +import 'package:novyronst/pages/widgets/ny_loading_widget.dart'; +import 'package:novyronst/root/lib_export.dart'; +import 'package:novyronst/tools/util/ny_device_info.dart'; +import 'package:novyronst/tools/widgets/extend_widget.dart'; +import 'package:novyronst/tools/widgets/novy_app_bar.dart'; +import 'package:novyronst/tools/widgets/novy_com_widget.dart'; +import 'package:url_launcher/url_launcher.dart'; + + +class AboutUsPage extends StatelessWidget { + + const AboutUsPage({super.key}); + + @override + Widget build(BuildContext context) { + // return NyLoadingWidget(); + return Scaffold( + backgroundColor: ConstColor.withValue('#110D08'), + appBar: NovyAppBar( + title: 'About', + titleColor: Colors.white, + ), + body: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 50), + SizedBox( + width: 80, height: 80, + child: HelpImage.asset(ConstImage.appLogo), + ), + const SizedBox(height: 10), + NovyText( ApiString.appName, + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + const SizedBox(height: 5), + NovyText( 'V ${NyDeviceInfo.instance.appVersion}', + fontSize: 12, + color: Colors.grey, + ), + const SizedBox(height: 20), + _buildRowItem('Privacy Policy', () { + AppNav.to(MineLinkPage( + title: 'Privacy Policy', + urlString: ApiString.webPrivacy, + )); + }), + + _buildRowItem('User Agreement', () { + AppNav.to(MineLinkPage( + title: 'User Agreement', + urlString: ApiString.webAgreement, + )); + + }), + + _buildRowItem('Visit Website', () async { + final encodedUrl = Uri.encodeFull(ApiString.webIndex); + try { + final bool launched = await launchUrl( + Uri.parse(encodedUrl), + mode: LaunchMode.externalApplication, + ); + if (!launched) { + print('无法打开链接: $encodedUrl'); + } + } catch (e) { + print('打开链接异常: $e'); + } + }), + + ], + ), + ), + ); + } + + _buildRowItem(String title, Function() onTap) { + return Container( + height: 46, + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), + margin: const EdgeInsets.only(bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + NovyText( title, fontSize: 14, color: Colors.white), + Icon(Icons.arrow_forward_ios, color: ConstColor.btWhite, size: 18,), + ], + ), + ).addInkWell(onTap: onTap); + } + + Future openUrl(String url) async { + final encodedUrl = Uri.encodeFull(url); + try { + final bool launched = await launchUrl( + Uri.parse(encodedUrl), + mode: LaunchMode.externalApplication, + ); + if (!launched) { + print('无法打开链接: $encodedUrl'); + } + } catch (e) { + print('打开链接异常: $e'); + } + } +} \ No newline at end of file diff --git a/lib/pages/my/pages/mine_history_page.dart b/lib/pages/my/pages/mine_history_page.dart new file mode 100644 index 0000000..6da343b --- /dev/null +++ b/lib/pages/my/pages/mine_history_page.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:novyronst/common/api/video_request.dart'; +import 'package:novyronst/common/model/ny_drama_model.dart'; +import 'package:novyronst/pages/collect/pages/collect_page_helpers.dart'; +import 'package:novyronst/pages/widgets/ny_empty_view.dart'; +import 'package:novyronst/pages/widgets/ny_loading_widget.dart'; +import 'package:novyronst/tools/widgets/novy_app_bar.dart'; + +import '../../../common/const/const_color.dart'; + +class MineHistoryPage extends StatefulWidget { + const MineHistoryPage({super.key}); + + @override + State createState() => _HistoryPageState(); +} + +class _HistoryPageState extends State { + List _itemsHis = []; + bool _isLoading = false; + bool _isEditing = false; + + @override + void initState() { + super.initState(); + _loadHistory(); + } + + Future _loadHistory() async { + setState(() { + _isLoading = true; + }); + + final fetch = VideoRequest.getHistory; + final items = await fetch(page: 1, pageSize: 20); + if (!mounted) return; + + setState(() { + _itemsHis + ..clear() + ..addAll(items); + _isLoading = false; + }); + } + + void _deleteHistory(int index) { + if (index < 0 || index >= _itemsHis.length) return; + setState(() { + _itemsHis.removeAt(index); + }); + } + + @override + Widget build(BuildContext context) { + if (_isLoading && _itemsHis.isEmpty) { + return const NyLoadingWidget(); + } + + if (_itemsHis.isEmpty) { + return const NyEmptyView(); + } + + return Scaffold( + backgroundColor: ConstColor.withValue('#110D08'), + appBar: NovyAppBar( + title: 'History', + titleColor: Colors.white, + actions: [ + IconButton( + onPressed: () { + setState(() { + _isEditing = !_isEditing; + }); + }, + icon: Icon( + _isEditing ? Icons.done : Icons.edit_outlined, + color: Colors.white, + size: 22, + ), + ) + ], + ), + body: ListView.separated( + padding: const EdgeInsets.fromLTRB(30, 8, 20, 96), + itemCount: _itemsHis.length, + separatorBuilder: (context, index) => const SizedBox(height: 13), + itemBuilder: (context, index) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 30, + child: _HistoryLeadingButton( + isEditing: _isEditing, + onDelete: () => _deleteHistory(index), + ), + ), + const SizedBox(width: 18), + Expanded(child: _HistoryItem(item: _itemsHis[index])), + ], + ); + }, + ), + ); + } +} + +class _HistoryLeadingButton extends StatelessWidget { + const _HistoryLeadingButton({ + required this.isEditing, + required this.onDelete, + }); + + final bool isEditing; + final VoidCallback onDelete; + + @override + Widget build(BuildContext context) { + if (isEditing) { + return IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor(width: 30, height: 40), + onPressed: onDelete, + icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 28), + ); + } + + return Icon( + Icons.bookmark_border, + color: Colors.white.withValues(alpha: 0.28), + size: 32, + ); + } +} + +class _HistoryItem extends StatelessWidget { + const _HistoryItem({required this.item}); + + final NyDramaModel 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.name, + 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( + dramaEpisodeText(item), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.45), + fontSize: 12, + fontFamily: 'Montserrat-Medium', + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/my/pages/mine_link_page.dart b/lib/pages/my/pages/mine_link_page.dart new file mode 100644 index 0000000..87c3b13 --- /dev/null +++ b/lib/pages/my/pages/mine_link_page.dart @@ -0,0 +1,298 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:novyronst/common/const/const_color.dart'; +import 'package:novyronst/pages/widgets/ny_loading_widget.dart'; +import 'package:novyronst/tools/widgets/novy_app_bar.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + + + +/// WebView 页面 +class MineLinkPage extends StatefulWidget { + + final String urlString; + final String? title; + final bool showAppbar; + + const MineLinkPage({ + super.key, + required this.urlString, + this.title, + this.showAppbar = true, + }); + + @override + State createState() => _WebLinkViewState(); +} + +class _WebLinkViewState extends State { + late WebViewController _webViewController; + int _loadingProgress = 0; + bool _isLoading = true; + bool _hasError = false; + String _errorMessage = ''; + String _urlString = ''; + + @override + void initState() { + super.initState(); + _initializeParameters(); + _initializeWebView(); + _loadUrl(); + } + + void _initializeParameters() { + _urlString = _validateAndFixUrl(widget.urlString); + } + + void _initializeWebView() { + _webViewController = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00FF0000)) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + setState(() { + _loadingProgress = progress; + }); + }, + onPageStarted: (String url) { + setState(() { + _isLoading = true; + _hasError = false; + _loadingProgress = 0; + }); + }, + onPageFinished: (String url) async { + setState(() { + _isLoading = false; + _loadingProgress = 100; + }); + + // 修改网页背景为透明,文字为白色 + await _webViewController.runJavaScript(''' + document.body.style.background = 'none'; + document.body.style.color = '#fff'; + + // 修改标题样式 + var titleElements = document.getElementsByClassName('title'); + for (var i = 0; i < titleElements.length; i++) { + titleElements[i].style.display = 'none'; + } + + // 修改文本背景为白色 + var bgElements = document.getElementsByClassName('bg-fff'); + for (var i = 0; i < bgElements.length; i++) { + bgElements[i].style.background = 'none'; + bgElements[i].style.paddingTop = '0px'; + } + + // 修改所有文本元素为白色 + var allElements = document.getElementsByTagName('*'); + for (var i = 0; i < allElements.length; i++) { + allElements[i].style.color = '#fff'; + } + + // 修改链接颜色 + var links = document.getElementsByTagName('a'); + for (var i = 0; i < links.length; i++) { + links[i].style.color = '#87CEEB'; + } + '''); + }, + onHttpError: (HttpResponseError error) { + setState(() { + _hasError = true; + _errorMessage = 'HTTP Error: ${error.response?.statusCode ?? "Unknown"}'; + _isLoading = false; + }); + }, + onWebResourceError: (WebResourceError error) { + setState(() { + _hasError = true; + _errorMessage = error.description; + _isLoading = false; + }); + }, + onNavigationRequest: (NavigationRequest request) { + return NavigationDecision.navigate; + }, + ), + ); + } + + void _loadUrl() { + if (_urlString.isEmpty) { + setState(() { + _hasError = true; + _errorMessage = 'Invalid URL'; + _isLoading = false; + }); + return; + } + + try { + final uri = Uri.parse(_urlString); + _webViewController.loadRequest(uri); + } catch (e) { + setState(() { + _hasError = true; + _errorMessage = 'Failed to load URL: $e'; + _isLoading = false; + }); + } + } + + String _validateAndFixUrl(String url) { + if (url.isEmpty) { + return ''; + } + + url = url.trim(); + + // Already has a valid scheme + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + + // Looks like a domain (has a dot and no spaces) + if (url.contains('.') && !url.contains(' ')) { + return 'https://$url'; + } + + // Treat as search query + return 'https://www.google.com/search?q=${Uri.encodeComponent(url)}'; + } + + void _reload() { + setState(() { + _hasError = false; + _isLoading = true; + _loadingProgress = 0; + _errorMessage = ''; + }); + _initializeWebView(); + _loadUrl(); + } + + void _goBack() { + _webViewController.goBack(); + } + + void _goForward() { + _webViewController.goForward(); + } + + @override + void dispose() { + _webViewController.clearCache(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBodyBehindAppBar: true, + appBar: widget.showAppbar + ? _buildAppBar() + : null, + body: _buildBody(), + ); + } + + PreferredSizeWidget _buildAppBar() { + return NovyAppBar( + backgroundColor: Colors.transparent, + title: widget.title, + titleColor: Colors.white, + actions: [ + IconButton( + icon: const Icon(Icons.refresh, color: Colors.white,), + onPressed: _reload, + ), + ], + ); + } + + Widget _buildBody() { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + ConstColor.black, + Color(0xFF2D1B4E), + ], + ), + ), + child: SafeArea( + child: Column( + children: [ + // 加载进度条(可选) + if (_isLoading) + LinearProgressIndicator( + value: _loadingProgress / 100, + backgroundColor: Colors.white.withOpacity(0.2), + valueColor: AlwaysStoppedAnimation( + ConstColor.btGreen, + ), + ), + Expanded( + child: _buildContent(), + ), + ], + ), + ), + ); + } + + Widget _buildContent() { + if (_isLoading) { + return Center( + child: NyLoadingWidget(), + ); + } + + if (_hasError) { + return _buildErrorWidget(); + } + + return WebViewWidget( + controller: _webViewController, + ); + } + + Widget _buildErrorWidget() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64.w, + color: Colors.white.withOpacity(0.6), + ), + SizedBox(height: 16.h), + Text( + _errorMessage, + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 16.sp, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 24.h), + ElevatedButton( + onPressed: _reload, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFB98E0), + foregroundColor: Colors.white, + ), + child: const 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 255b0eb..cb476f2 100644 --- a/lib/pages/my/pages/my_page.dart +++ b/lib/pages/my/pages/my_page.dart @@ -1,8 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:novyronst/common/api/api_string.dart'; +import 'package:novyronst/common/api/user_request.dart'; +import 'package:novyronst/common/help/app_nav.dart'; import 'package:novyronst/common/help/help_image.dart'; +import 'package:novyronst/common/model/novyronst_user.dart'; +import 'package:novyronst/pages/my/pages/about_page.dart'; +import 'package:novyronst/pages/my/pages/mine_history_page.dart'; +import 'package:novyronst/pages/my/pages/mine_link_page.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/app_toast_util.dart'; import 'package:novyronst/tools/widgets/extend_widget.dart'; import 'package:novyronst/tools/widgets/novy_com_widget.dart'; @@ -17,9 +26,25 @@ class MyPage extends StatefulWidget { class _RootPageState extends State { - List _titles = [ + final List _titles = [ 'History', 'Settings', 'About', 'Privacy Policy', 'User Agreement' ]; + NovyronstUser? _userInfo; + + @override + void initState() { + super.initState(); + + _getUserInfo(); + } + + void _getUserInfo() { + UserRequest.getUserInfo().then((value) { + setState(() { + _userInfo = value; + }); + }); + } @override Widget build(BuildContext context) { @@ -35,15 +60,21 @@ class _RootPageState extends State { ...List.generate(_titles.length, (index) { return _buildSectionView(_titles[index], index, onTap: () { if (index == 0) { - + AppNav.to(MineHistoryPage()); } else if (index == 1) { } else if (index == 2) { + AppNav.to(AboutUsPage()); + } else if (index == 3) { - + AppNav.to(MineLinkPage(urlString: ApiString.webPrivacy, + title: 'Privacy Policy', + )); } else if (index == 4) { - + AppNav.to(MineLinkPage(urlString: ApiString.webAgreement, + title: 'User Agreement', + )); } }); }) @@ -91,6 +122,16 @@ class _RootPageState extends State { } Widget _buildUserInfo() { + + String _name = 'Visitor'; + String _customerId = ''; + if (_userInfo != null) { + _name = _userInfo!.givingName!.isEmpty || _userInfo!.givingName=='' + ? 'Visitor' + : _userInfo!.givingName!; + _customerId = _userInfo!.customerId; + } + return Row( children: [ Container( @@ -108,7 +149,7 @@ class _RootPageState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - NovyText('Visitor', + NovyText(_name, fontSize: 18, ), const SizedBox(height: 6), @@ -121,14 +162,19 @@ class _RootPageState extends State { ), child: Row( children: [ - NovyText('ID: 3242545454', + NovyText('ID: $_customerId', fontSize: 11, ), const SizedBox(width: 6), HelpImage.asset(ConstImage.copy, width: 12, height: 12) ], ), - ) + ).addInkWell(onTap: () async { + // copy + if (_userInfo!.customerId.isEmpty) return; + await Clipboard.setData(ClipboardData(text: _userInfo!.customerId)); + AppToastUtil.showSuccess('ID is Copied!'); + }), ], ) ], diff --git a/lib/pages/play/pages/play_page.dart b/lib/pages/play/pages/play_page.dart index d899b80..0d08482 100644 --- a/lib/pages/play/pages/play_page.dart +++ b/lib/pages/play/pages/play_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:novyronst/common/api/video_request.dart'; import 'package:novyronst/common/model/ny_drama_model.dart'; import 'package:novyronst/pages/play/widgets/player_view.dart'; +import 'package:novyronst/pages/widgets/ny_loading_widget.dart'; class PlayPage extends StatefulWidget { const PlayPage({super.key, this.isActive = true}); @@ -69,33 +70,26 @@ class _RootPageState extends State { if (index < 0 || index >= _videoList.length) return; final item = _videoList[index]; - final videoId = item.shortPlayVideoId ?? item.id; - if (videoId == null) return; + final _info = item.videoInfo ?? {}; + final videoId = _info['short_play_video_id']; + if (videoId == null) { + return; + } + final shortPlayId = _info['short_play_id']; + if (shortPlayId == null) { + return; + } - final shortPlayId = item.shortPlayId ?? item.shortId; - final oldIsCollect = item.isCollect ?? false; - final oldCollectTotal = item.collectTotal; - final newIsCollect = !oldIsCollect; - final newCollectTotal = oldCollectTotal + (newIsCollect ? 1 : -1); + final success = item.isCollect == false + ? await VideoRequest.collect(shortPlayId: shortPlayId ?? 0, videoId: videoId) + : await VideoRequest.cancelCollect(shortPlayId: shortPlayId ?? 0); + + if (!mounted) return; setState(() { - _videoList[index] = item.copyWith( - isCollect: newIsCollect, - collectTotal: newCollectTotal < 0 ? 0 : newCollectTotal, - ); - }); - final success = newIsCollect - ? await VideoRequest.collect(shortPlayId: shortPlayId, videoId: videoId) - : await VideoRequest.cancelCollect(shortPlayId: shortPlayId); - - if (!mounted || success) return; - - setState(() { - _videoList[index] = _videoList[index].copyWith( - isCollect: oldIsCollect, - collectTotal: oldCollectTotal, - ); + _videoList[index] = item.copyWith(isCollect: !item.isCollect!); + // item.isCollect = !item.isCollect!; }); } @@ -106,9 +100,7 @@ class _RootPageState extends State { Widget _buildBody() { if (_isLoading && _videoList.isEmpty) { - return const Center( - child: CircularProgressIndicator(color: Colors.white), - ); + return const NyLoadingWidget(); } if (_videoList.isEmpty) { diff --git a/lib/pages/play/widgets/player_view.dart b/lib/pages/play/widgets/player_view.dart index c6a43f7..f2d7f7b 100644 --- a/lib/pages/play/widgets/player_view.dart +++ b/lib/pages/play/widgets/player_view.dart @@ -13,6 +13,8 @@ class PlayerView extends StatefulWidget { this.onCollectTap, this.isLoadingMore = false, this.isActive = true, + this.bottomMetaSpacing = 76, + this.progressHeaderBuilder, }); final List videos; @@ -21,6 +23,9 @@ class PlayerView extends StatefulWidget { final ValueChanged? onCollectTap; final bool isLoadingMore; final bool isActive; + final double bottomMetaSpacing; + final Widget Function(BuildContext context, NyDramaModel drama, int index)? + progressHeaderBuilder; @override State createState() => _PlayerViewState(); @@ -48,6 +53,13 @@ class _PlayerViewState extends State { super.didUpdateWidget(oldWidget); if (widget.videos.isEmpty) return; + if (oldWidget.initialIndex != widget.initialIndex) { + _currentIndex = widget.initialIndex.clamp(0, widget.videos.length - 1); + if (_pageController.hasClients) { + _jumpToPageAfterBuild(_currentIndex); + } + } + if (_currentIndex >= widget.videos.length) { _currentIndex = widget.videos.length - 1; } @@ -187,12 +199,24 @@ class _PlayerViewState extends State { } void _handlePageChanged(int index) { + if (index == _currentIndex) return; _currentIndex = index; widget.onPageChanged?.call(index); _cachePlaybackWindow(index); widget.isActive ? _playActiveVideo() : _pauseAllVideos(); } + void _jumpToPageAfterBuild(int index) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || widget.videos.isEmpty || !_pageController.hasClients) { + return; + } + + final nextIndex = index.clamp(0, widget.videos.length - 1); + _pageController.jumpToPage(nextIndex); + }); + } + void _togglePlayback() { final controller = _controllers[_currentIndex]; if (!widget.isActive || @@ -217,12 +241,15 @@ class _PlayerViewState extends State { itemBuilder: (context, index) { return _PlayerPage( drama: widget.videos[index], + index: index, controller: _controllers[index], isPageActive: widget.isActive, isLoadingMore: widget.isLoadingMore && index == widget.videos.length - 1, + bottomMetaSpacing: widget.bottomMetaSpacing, onTap: _togglePlayback, onCollectTap: () => widget.onCollectTap?.call(index), + progressHeaderBuilder: widget.progressHeaderBuilder, ); }, ); @@ -232,23 +259,33 @@ class _PlayerViewState extends State { class _PlayerPage extends StatelessWidget { const _PlayerPage({ required this.drama, + required this.index, required this.controller, required this.isPageActive, required this.isLoadingMore, + required this.bottomMetaSpacing, required this.onTap, required this.onCollectTap, + required this.progressHeaderBuilder, }); final NyDramaModel drama; + final int index; final VideoPlayerController? controller; final bool isPageActive; final bool isLoadingMore; + final double bottomMetaSpacing; final VoidCallback onTap; final VoidCallback onCollectTap; + final Widget Function(BuildContext context, NyDramaModel drama, int index)? + progressHeaderBuilder; @override Widget build(BuildContext context) { - final bottomPadding = MediaQuery.paddingOf(context).bottom + 86; + final metaBottom = resolvePlayerMetaBottom( + safeAreaBottom: MediaQuery.paddingOf(context).bottom, + bottomSpacing: bottomMetaSpacing, + ); return GestureDetector( behavior: HitTestBehavior.opaque, @@ -263,14 +300,14 @@ class _PlayerPage extends StatelessWidget { Positioned( left: 16, right: 16, - bottom: bottomPadding - 10, - child: _buildMeta(), + bottom: metaBottom, + child: _buildMeta(context), ), if (isLoadingMore) Positioned( left: 0, right: 0, - bottom: bottomPadding - 30, + bottom: metaBottom - 20, child: const Center( child: SizedBox( width: 18, @@ -324,7 +361,7 @@ class _PlayerPage extends StatelessWidget { ); } - Widget _buildMeta() { + Widget _buildMeta(BuildContext context) { final isCollect = drama.isCollect ?? false; return Column( mainAxisSize: MainAxisSize.min, @@ -370,6 +407,10 @@ class _PlayerPage extends StatelessWidget { ], ), const SizedBox(height: 16), + if (progressHeaderBuilder != null) ...[ + progressHeaderBuilder!(context, drama, index), + const SizedBox(height: 8), + ], _buildProgress(), ], ); @@ -475,3 +516,10 @@ bool shouldPlayActiveVideo( value.isInitialized && !value.isPlaying; } + +double resolvePlayerMetaBottom({ + required double safeAreaBottom, + required double bottomSpacing, +}) { + return safeAreaBottom + bottomSpacing; +} diff --git a/lib/pages/widgets/ny_loading_widget.dart b/lib/pages/widgets/ny_loading_widget.dart new file mode 100644 index 0000000..b04a7b7 --- /dev/null +++ b/lib/pages/widgets/ny_loading_widget.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:novyronst/common/const/const_color.dart'; +import 'package:novyronst/root/lib_export.dart'; + +class NyLoadingWidget extends StatefulWidget { + final double size; + + const NyLoadingWidget({ + super.key, + this.size = 10, + }); + + @override + State createState() => _ThreeDotLoadingState(); +} + +class _ThreeDotLoadingState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + int _currentIndex = 0; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + )..addListener(() { + // 每完成一次动画循环,更新索引 + if (_controller.isCompleted) { + _controller.reset(); + setState(() { + _currentIndex = (_currentIndex + 1) % 3; + }); + _controller.forward(); + } + }); + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: AppScreen.screenWidth, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(3, (index) { + final isDark = index == _currentIndex; + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.symmetric(horizontal: 6), + width: widget.size, + height: widget.size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDark ? ConstColor.withValue('#89F011') : ConstColor.withValue('#E2FFC0'), + ), + ); + }), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/widgets/ny_tag_widget.dart b/lib/pages/widgets/ny_tag_widget.dart index 3cab449..c4c81c7 100644 --- a/lib/pages/widgets/ny_tag_widget.dart +++ b/lib/pages/widgets/ny_tag_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.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'; @@ -16,6 +17,10 @@ class NyTagWidget extends StatelessWidget { @override Widget build(BuildContext context) { + String _tag = text; + if (text.length > 12) { + _tag = text.substring(0, 12); + } return Container( height: 20, padding: const EdgeInsets.symmetric(horizontal: 8), @@ -23,8 +28,9 @@ class NyTagWidget extends StatelessWidget { color: bgColor ?? ConstColor.withValue('EEEEEE'), borderRadius: BorderRadius.circular(4), ), + // constraints: BoxConstraints(maxWidth: 100.w), child: Center( - child: NovyText(text, + child: NovyText(_tag, fontSize: 10, color: textColor ?? ConstColor.black, ), diff --git a/pubspec.lock b/pubspec.lock index 5a0343a..55a02b2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -717,6 +717,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c" + url: "https://pub.dev" + source: hosted + version: "6.3.30" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" url_launcher_linux: dependency: transitive description: @@ -725,6 +749,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" url_launcher_platform_interface: dependency: transitive description: @@ -821,6 +853,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: e4d3474277c5043f5f3100ed27d94ad693abfd5c602b0221c719a4497e4ba1e4 + url: "https://pub.dev" + source: hosted + version: "4.14.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: ad5182eff9a550925330cb9f0cb038eddfdd5712aba8b77aa0f0400e50f6e688 + url: "https://pub.dev" + source: hosted + version: "4.12.0" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" + url: "https://pub.dev" + source: hosted + version: "2.15.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2" + url: "https://pub.dev" + source: hosted + version: "3.25.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c7c3ce9..f242133 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,9 @@ dependencies: video_player: ^2.10.0 easy_refresh: ^3.4.0 + webview_flutter: ^4.13.0 + url_launcher: ^6.3.2 +