diff --git a/assets/imgs/home_head.png b/assets/imgs/home_head.png new file mode 100644 index 0000000..5bb6528 Binary files /dev/null and b/assets/imgs/home_head.png differ diff --git a/assets/other/app_logo.png b/assets/other/app_logo.png new file mode 100644 index 0000000..7759ae2 Binary files /dev/null and b/assets/other/app_logo.png differ diff --git a/lib/common/help/help_image.dart b/lib/common/help/help_image.dart index 8aa7bf1..833d908 100644 --- a/lib/common/help/help_image.dart +++ b/lib/common/help/help_image.dart @@ -37,6 +37,8 @@ class ConstImage { // remove static const String remove = "assets/imgs/remove.png"; static const String homeEyes = "assets/imgs/home_eyes.png"; + // home_head + static const String homeHead = "assets/imgs/home_head.png"; /// other static const String mineBg = "assets/other/mine_bg.png"; @@ -51,6 +53,8 @@ class ConstImage { static const String primary = "assets/other/pricary.png"; static const String webSite = "assets/other/web_site.png"; static const String emptyImg = "assets/other/empty_img.png"; + // app_logo + static const String appLogo = "assets/other/app_logo.png"; } diff --git a/lib/pages/collect/pages/collect_page_helpers.dart b/lib/pages/collect/pages/collect_page_helpers.dart new file mode 100644 index 0000000..3f15f10 --- /dev/null +++ b/lib/pages/collect/pages/collect_page_helpers.dart @@ -0,0 +1,14 @@ +import 'package:novyronst/common/model/ny_drama_model.dart'; + +int collectShortPlayId(NyDramaModel drama) { + return drama.shortPlayId ?? drama.shortId; +} + +String dramaEpisodeText(NyDramaModel drama) { + final currentEpisode = drama.currentEpisode ?? drama.episode ?? 1; + return 'EP.$currentEpisode/EP.${drama.episodeTotal}'; +} + +String collectionTabLabel(int index) { + return index == 1 ? 'History' : 'Favorites'; +} diff --git a/lib/pages/collect/pages/collection_page.dart b/lib/pages/collect/pages/collection_page.dart index f0bd1e0..250d046 100644 --- a/lib/pages/collect/pages/collection_page.dart +++ b/lib/pages/collect/pages/collection_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; - -import '../../../tools/widgets/app_bg_page.dart'; +import 'package:novyronst/pages/collect/pages/collect_page_helpers.dart'; +import 'package:novyronst/pages/collect/pages/favorite_page.dart'; +import 'package:novyronst/pages/collect/pages/history_page.dart'; +import 'package:novyronst/tools/widgets/app_bg_page.dart'; class CollectionPage extends StatefulWidget { const CollectionPage({super.key}); @@ -12,125 +14,27 @@ class CollectionPage extends StatefulWidget { } class _RootPageState extends State { - 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 +