feat: 视频详情页,分类页。个人中心
This commit is contained in:
parent
b5bce6edcf
commit
a3426831ed
BIN
assets/imgs/home_head.png
Normal file
BIN
assets/imgs/home_head.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/other/app_logo.png
Normal file
BIN
assets/other/app_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@ -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";
|
||||
|
||||
}
|
||||
|
||||
|
||||
14
lib/pages/collect/pages/collect_page_helpers.dart
Normal file
14
lib/pages/collect/pages/collect_page_helpers.dart
Normal file
@ -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';
|
||||
}
|
||||
@ -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<CollectionPage> {
|
||||
static const List<_DramaItem> _favoriteItems = [
|
||||
_DramaItem(
|
||||
title: 'An Uninvited Guest',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/09/26/66f4d0ce67383.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Vanished Without a Word',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/06/11/6667f17c9df69.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Dawn of Stardom',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/07/19/669a15b8a2e28.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Marry Me Twice',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/04/30/6630c89b746a8.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Not Your Bride',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/05/22/664daf9d042d4.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: "Husband's Hidden Crown",
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/08/12/66b9dfd8182cc.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'In the City of Love',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/07/24/66a0b88fda163.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: "Mommy's Genius Squad",
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/05/17/664716f187b2b.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Burned by Love',
|
||||
imageUrl:
|
||||
'https://static.goodshort.com/upload/serial/cover/2024/07/05/66876b720daf4.jpg',
|
||||
currentEpisode: 1,
|
||||
totalEpisode: 51,
|
||||
),
|
||||
];
|
||||
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<CollectionPage> {
|
||||
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<CollectionPage> {
|
||||
|
||||
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<CollectionPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
194
lib/pages/collect/pages/favorite_page.dart
Normal file
194
lib/pages/collect/pages/favorite_page.dart
Normal file
@ -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<List<NyDramaModel>> Function({int page, int pageSize})?
|
||||
fetchCollections;
|
||||
final Future<bool> Function({required int shortPlayId})? deleteCollection;
|
||||
|
||||
@override
|
||||
State<FavoritePage> createState() => _FavoritePageState();
|
||||
}
|
||||
|
||||
class _FavoritePageState extends State<FavoritePage> {
|
||||
final List<NyDramaModel> _items = [];
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadFavorites();
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
179
lib/pages/collect/pages/history_page.dart
Normal file
179
lib/pages/collect/pages/history_page.dart
Normal file
@ -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<List<NyDramaModel>> Function({int page, int pageSize})?
|
||||
fetchHistory;
|
||||
|
||||
@override
|
||||
State<HistoryPage> createState() => _HistoryPageState();
|
||||
}
|
||||
|
||||
class _HistoryPageState extends State<HistoryPage> {
|
||||
final List<NyDramaModel> _items = [];
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadHistory();
|
||||
}
|
||||
|
||||
Future<void> _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
280
lib/pages/first/pages/category_list_page.dart
Normal file
280
lib/pages/first/pages/category_list_page.dart
Normal file
@ -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<CategoryListPage> createState() => _CatePageState();
|
||||
}
|
||||
|
||||
class _CatePageState extends State<CategoryListPage> {
|
||||
final List<NyCategoryModel> _categories = [];
|
||||
final List<NyDramaModel> _dramas = [];
|
||||
int _selectedIndex = 0;
|
||||
int? _loadingCategoryId;
|
||||
bool _loadingCategories = true;
|
||||
bool _loadingDramas = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCategories();
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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<NyCategoryModel> 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;
|
||||
}
|
||||
@ -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<FirstPage> {
|
||||
|
||||
List<NyCategoryModel> _categories = [];
|
||||
List<NyHomeModel> _homeDatas = [];
|
||||
final EasyRefreshController _refreshController = EasyRefreshController(
|
||||
controlFinishRefresh: true,
|
||||
);
|
||||
final List<String> _cateTitles = [
|
||||
'Counterattack Life',
|
||||
'Reborn and Returned',
|
||||
'God of War Returns',
|
||||
'System Flow',
|
||||
];
|
||||
|
||||
final List<String> _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<FirstPage> {
|
||||
_homeDatas = value;
|
||||
});
|
||||
|
||||
for (NyHomeModel element in value) {
|
||||
print('ele len===: ${element.moduleKey} : ${element.dataList.length}');
|
||||
List<NyDramaModel> 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<FirstPage> {
|
||||
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<ScrollNotification>(
|
||||
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<Widget> _buildSliversFromData() {
|
||||
final List<Widget> slivers = [];
|
||||
|
||||
int index = 0;
|
||||
for (final str in _cateTags) {
|
||||
List<NyDramaModel> 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;
|
||||
}
|
||||
|
||||
291
lib/pages/first/player_detail/video_detail_page.dart
Normal file
291
lib/pages/first/player_detail/video_detail_page.dart
Normal file
@ -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<VideoDetailPage> createState() => _VideoDetailPageState();
|
||||
}
|
||||
|
||||
class _VideoDetailPageState extends State<VideoDetailPage> {
|
||||
NyDetailModel? _detail;
|
||||
List<NyDramaModel> _videos = [];
|
||||
int _currentIndex = 0;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadDetail();
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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<NyDramaModel> 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<NyEpisodeModel> episodes,
|
||||
NyEpisodeModel selectedEpisode,
|
||||
) {
|
||||
return episodes.indexWhere(
|
||||
(episode) => episode.shortPlayVideoId == selectedEpisode.shortPlayVideoId,
|
||||
);
|
||||
}
|
||||
|
||||
int episodeIndexForDrama(List<NyEpisodeModel> 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;
|
||||
}
|
||||
217
lib/pages/first/player_detail/video_esp_page.dart
Normal file
217
lib/pages/first/player_detail/video_esp_page.dart
Normal file
@ -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<NyEpisodeModel>? onSelectedEpisode;
|
||||
|
||||
@override
|
||||
State<VideoEspPage> createState() => _VideoEspPageState();
|
||||
}
|
||||
|
||||
class _VideoEspPageState extends State<VideoEspPage> {
|
||||
late int _selectedEpisode;
|
||||
|
||||
NyDramaModel? get _drama =>
|
||||
widget.detail.shortPlayInfo ?? widget.detail.videoInfo;
|
||||
|
||||
NyDramaModel? get _info => widget.detail.videoInfo;
|
||||
|
||||
List<NyEpisodeModel> 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;
|
||||
}
|
||||
}
|
||||
99
lib/pages/first/widgets/home_card_widget.dart
Normal file
99
lib/pages/first/widgets/home_card_widget.dart
Normal file
@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:novyronst/common/help/app_nav.dart';
|
||||
import 'package:novyronst/common/help/help_image.dart';
|
||||
import 'package:novyronst/common/model/ny_drama_model.dart';
|
||||
import 'package:novyronst/pages/first/player_detail/video_detail_page.dart';
|
||||
import 'package:novyronst/pages/widgets/ny_tag_widget.dart';
|
||||
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
|
||||
|
||||
import '../../../tools/util/app_screen.dart';
|
||||
import '../../../tools/widgets/novy_button_bg.dart';
|
||||
import '../../widgets/cut_corner_clipper.dart';
|
||||
import 'home_remaining_sections.dart';
|
||||
|
||||
class HomeCardWidget extends StatelessWidget {
|
||||
const HomeCardWidget({
|
||||
super.key,
|
||||
required this.dramaList,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final List<NyDramaModel> dramaList;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CutCornerClipper(
|
||||
width: AppScreen.screenWidth - 32,
|
||||
height: 308.h,
|
||||
cutCorner: CutCorner.topRight,
|
||||
cutSize: 30,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_threeLine(),
|
||||
const SizedBox(height: 10),
|
||||
OutlinedTitle(title, fontSize: 17),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(child: _listView()),
|
||||
const SizedBox(height: 6),
|
||||
NovyButtonBg(text: 'Click to watch', width: 327.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _listView() {
|
||||
return ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: dramaList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final drama = dramaList[index];
|
||||
var tag = '';
|
||||
if (drama.category != null && drama.category!.isNotEmpty) {
|
||||
tag = drama.category!.first;
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => AppNav.to(VideoDetailPage(drama: drama)),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 7),
|
||||
width: 104.w,
|
||||
child: Column(
|
||||
children: [
|
||||
HelpImage.network(drama.imageUrl, width: 104.w, height: 157.h),
|
||||
const SizedBox(height: 5),
|
||||
NovyText(
|
||||
drama.name,
|
||||
fontSize: 12.sp,
|
||||
color: Colors.black,
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
if (tag != '') Row(children: [NyTagWidget(text: tag)]),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _threeLine() {
|
||||
return Row(
|
||||
children: List.generate(3, (index) {
|
||||
return Container(
|
||||
width: 3,
|
||||
height: 8,
|
||||
color: Colors.black,
|
||||
margin: const EdgeInsets.only(right: 3),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:novyronst/common/help/app_nav.dart';
|
||||
import 'package:novyronst/common/model/ny_category_model.dart';
|
||||
import 'package:novyronst/pages/first/pages/category_list_page.dart';
|
||||
import 'package:novyronst/pages/widgets/cut_corner_clipper.dart';
|
||||
import 'package:novyronst/pages/widgets/ny_progress_widget.dart';
|
||||
import 'package:novyronst/pages/widgets/ny_tag_widget.dart';
|
||||
import 'package:novyronst/root/lib_export.dart';
|
||||
import 'package:novyronst/tools/widgets/extend_widget.dart';
|
||||
import 'package:novyronst/tools/widgets/novy_button_bg.dart';
|
||||
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
|
||||
import 'package:novyronst/tools/widgets/novy_img_bg.dart';
|
||||
|
||||
class HomeFirstView extends StatefulWidget {
|
||||
|
||||
const HomeFirstView({super.key,
|
||||
required this.categories,
|
||||
});
|
||||
const HomeFirstView({super.key, required this.categories});
|
||||
|
||||
final List<NyCategoryModel> categories;
|
||||
|
||||
@ -24,21 +24,22 @@ class HomeFirstView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomeFirstViewState extends State<HomeFirstView> {
|
||||
|
||||
bool _hasHistory = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double _space = 25;
|
||||
const double space = 25;
|
||||
return Stack(
|
||||
children: [
|
||||
// 151
|
||||
SizedBox(
|
||||
height: 151 - _space + 200 + 52 + 20,
|
||||
SizedBox(height: 151 - space + 200 + 52 + 20),
|
||||
HelpImage.asset(
|
||||
ConstImage.homeFirst,
|
||||
width: AppScreen.screenWidth,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
HelpImage.asset(ConstImage.homeFirst, width: AppScreen.screenWidth, fit: BoxFit.fill),
|
||||
Positioned(
|
||||
top: 151-_space, left: 16, right: 16,
|
||||
top: 151 - space,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Column(
|
||||
children: [
|
||||
CutCornerClipper(
|
||||
@ -52,40 +53,46 @@ class _HomeFirstViewState extends State<HomeFirstView> {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(widget.categories.length, (index) {
|
||||
final _item = widget.categories[index];
|
||||
final item = widget.categories[index];
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: NovyImgBg(imgBg: ConstImage.cateBg,
|
||||
child: NovyImgBg(
|
||||
imgBg: ConstImage.cateBg,
|
||||
height: 52,
|
||||
child: Stack(
|
||||
children:[
|
||||
NovyText(_item.name, color: ConstColor.btGreen,
|
||||
children: [
|
||||
NovyText(
|
||||
item.name,
|
||||
color: ConstColor.btGreen,
|
||||
fontSize: 21,
|
||||
textAlign: TextAlign.center,
|
||||
fontFamily: ConstFont.montBlack,
|
||||
shadow: Shadow(
|
||||
offset: const Offset(1, 1), // X: 1, Y: 1
|
||||
blurRadius: 0, // Blur: 0
|
||||
color: Colors.black, // #000000
|
||||
blurRadius: 0, // Blur: 0
|
||||
color: Colors.black, // #000000
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0, left: 0, right: 0,
|
||||
child: Container(
|
||||
height: 2,
|
||||
color: Colors.black,
|
||||
),
|
||||
)
|
||||
]
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(height: 2, color: Colors.black),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
).addInkWell(
|
||||
onTap: () {
|
||||
AppNav.to(CategoryListPage(preCategory: item));
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -97,13 +104,14 @@ class _HomeFirstViewState extends State<HomeFirstView> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
NovyText('Current Viewing Progress',
|
||||
NovyText(
|
||||
'Current Viewing Progress',
|
||||
color: Colors.black,
|
||||
fontSize: 16,
|
||||
fontFamily: ConstFont.montBlack,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
HelpImage.asset(ConstImage.homeCurrentTag, height: 10)
|
||||
HelpImage.asset(ConstImage.homeCurrentTag, height: 10),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
@ -111,7 +119,8 @@ class _HomeFirstViewState extends State<HomeFirstView> {
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.grey.withAlpha(200),
|
||||
width: 60, height: 90,
|
||||
width: 60,
|
||||
height: 90,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
@ -119,13 +128,15 @@ class _HomeFirstViewState extends State<HomeFirstView> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
NovyText('Jdksyjs dsaoy',
|
||||
NovyText(
|
||||
'Jdksyjs dsaoy',
|
||||
color: Colors.black,
|
||||
fontSize: 14,
|
||||
fontFamily: ConstFont.montMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
NovyText('Jdksyjs dsaoy dasku dsaldsa dljas',
|
||||
NovyText(
|
||||
'Jdksyjs dsaoy dasku dsaldsa dljas',
|
||||
color: Colors.grey,
|
||||
fontSize: 10,
|
||||
fontFamily: ConstFont.montRegular,
|
||||
@ -138,18 +149,18 @@ class _HomeFirstViewState extends State<HomeFirstView> {
|
||||
const SizedBox(width: 8),
|
||||
NyHotWidget(text: '12.4k'),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
NyProgressWidget(),
|
||||
const SizedBox(height: 6),
|
||||
NovyButtonBg(text: 'Keep Watching', type: 2, width: 327.w,)
|
||||
NovyButtonBg(text: 'Keep Watching', type: 2, width: 327.w),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:novyronst/common/const/const_color.dart';
|
||||
import 'package:novyronst/common/const/const_string.dart';
|
||||
import 'package:novyronst/common/help/app_nav.dart';
|
||||
import 'package:novyronst/common/help/help_image.dart';
|
||||
import 'package:novyronst/common/model/ny_drama_model.dart';
|
||||
import 'package:novyronst/pages/first/player_detail/video_detail_page.dart';
|
||||
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
|
||||
|
||||
import '../../../tools/util/app_screen.dart';
|
||||
@ -12,11 +14,7 @@ import '../../widgets/cut_corner_clipper.dart';
|
||||
import '../../widgets/ny_tag_widget.dart';
|
||||
|
||||
class HomeRankWidget extends StatelessWidget {
|
||||
|
||||
const HomeRankWidget({
|
||||
super.key,
|
||||
required this.dramaRanks,
|
||||
});
|
||||
const HomeRankWidget({super.key, required this.dramaRanks});
|
||||
|
||||
final List<NyDramaModel> dramaRanks;
|
||||
|
||||
@ -28,24 +26,25 @@ class HomeRankWidget extends StatelessWidget {
|
||||
cutSize: 40,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: dramaRanks.isEmpty ? SizedBox(height: 20,) : Column(
|
||||
children: [
|
||||
_buildRankList(),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 20.w),
|
||||
NovyButtonBg(text: 'Keep Watching', width: 308.w,)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
child: dramaRanks.isEmpty
|
||||
? const SizedBox(height: 20)
|
||||
: Column(
|
||||
children: [
|
||||
_buildRankList(),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 20.w),
|
||||
NovyButtonBg(text: 'Keep Watching', width: 308.w),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildRankList() {
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
_firstVideoView(),
|
||||
@ -56,85 +55,91 @@ class HomeRankWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _firstVideoView() {
|
||||
NyDramaModel _item = dramaRanks.first;
|
||||
String _tag = "Safiray";
|
||||
if (_item.categoryList!=null && _item.categoryList!.isNotEmpty) {
|
||||
_tag = _item.categoryList!.first.name;
|
||||
final item = dramaRanks.first;
|
||||
var tag = 'Safiray';
|
||||
if (item.categoryList != null && item.categoryList!.isNotEmpty) {
|
||||
tag = item.categoryList!.first.name;
|
||||
}
|
||||
return SizedBox(
|
||||
width: 160.w,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 160.w, height: 240.w,
|
||||
child: HelpImage.network(_item.imageUrl, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(
|
||||
top: -10, left: 1,
|
||||
child: _colorsText(1),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
NovyText(_item.name, fontSize: 15,
|
||||
color: Colors.black,
|
||||
fontFamily: ConstFont.montMedium,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
NyTagWidget(text: _tag),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => AppNav.to(VideoDetailPage(drama: item)),
|
||||
child: SizedBox(
|
||||
width: 160.w,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 160.w,
|
||||
height: 240.w,
|
||||
child: HelpImage.network(item.imageUrl, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(top: -10, left: 1, child: _colorsText(1)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
NovyText(
|
||||
item.name,
|
||||
fontSize: 15,
|
||||
color: Colors.black,
|
||||
fontFamily: ConstFont.montMedium,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Row(children: [NyTagWidget(text: tag)]),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _otherVideoView() {
|
||||
int _count = dramaRanks.length - 1;
|
||||
if (_count > 3) _count = 3;
|
||||
var count = dramaRanks.length - 1;
|
||||
if (count > 3) count = 3;
|
||||
return Column(
|
||||
children: List.generate(_count, (index) {
|
||||
NyDramaModel _item = dramaRanks[index + 1];
|
||||
String _tag = "";
|
||||
if (_item.categoryList!=null && _item.categoryList!.isNotEmpty) {
|
||||
_tag = _item.categoryList!.first.name;
|
||||
children: List.generate(count, (index) {
|
||||
final item = dramaRanks[index + 1];
|
||||
var tag = '';
|
||||
if (item.categoryList != null && item.categoryList!.isNotEmpty) {
|
||||
tag = item.categoryList!.first.name;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
width: 160.w, height: 93.w,
|
||||
child: Row(
|
||||
children: [
|
||||
HelpImage.network(_item.imageUrl, width: 62.w, height: 93.w),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_colorsText(index + 2),
|
||||
const SizedBox(height: 8),
|
||||
NovyText(_item.name, fontSize: 13,
|
||||
color: Colors.black,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
_tag.isNotEmpty ? NyTagWidget(text: _tag) : SizedBox()
|
||||
],
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => AppNav.to(VideoDetailPage(drama: item)),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
width: 160.w,
|
||||
height: 93.w,
|
||||
child: Row(
|
||||
children: [
|
||||
HelpImage.network(item.imageUrl, width: 62.w, height: 93.w),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_colorsText(index + 2),
|
||||
const SizedBox(height: 8),
|
||||
NovyText(item.name, fontSize: 13, color: Colors.black),
|
||||
const SizedBox(height: 5),
|
||||
tag.isNotEmpty
|
||||
? NyTagWidget(text: tag)
|
||||
: const SizedBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
_colorsText(int index) {
|
||||
Widget _colorsText(int index) {
|
||||
if (index == 4) {
|
||||
return Text(
|
||||
'$index',
|
||||
@ -160,7 +165,7 @@ class HomeRankWidget extends StatelessWidget {
|
||||
child: Text(
|
||||
'$index',
|
||||
style: TextStyle(
|
||||
fontSize: index==1 ? 40: 20,
|
||||
fontSize: index == 1 ? 40 : 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontStyle: FontStyle.italic,
|
||||
@ -168,4 +173,4 @@ class HomeRankWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:novyronst/common/api/video_request.dart';
|
||||
import 'package:novyronst/common/const/const_color.dart';
|
||||
import 'package:novyronst/common/const/const_string.dart';
|
||||
import 'package:novyronst/common/help/app_nav.dart';
|
||||
import 'package:novyronst/common/help/help_image.dart';
|
||||
import 'package:novyronst/common/model/ny_drama_model.dart';
|
||||
import 'package:novyronst/common/model/ny_episode_model.dart';
|
||||
import 'package:novyronst/pages/first/player_detail/video_detail_page.dart';
|
||||
import 'package:novyronst/pages/widgets/cut_corner_clipper.dart';
|
||||
import 'package:novyronst/pages/widgets/ny_tag_widget.dart';
|
||||
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
|
||||
|
||||
class HomeRemainingSections extends StatelessWidget {
|
||||
const HomeRemainingSections({super.key});
|
||||
import '../../../common/model/ny_detail_model.dart';
|
||||
|
||||
const int episodeSeriesPageSize = 6;
|
||||
|
||||
static const _ActorInfo _actor = _ActorInfo(
|
||||
class HomeRemainingSections extends StatefulWidget {
|
||||
const HomeRemainingSections({super.key, required this.dramaOne});
|
||||
|
||||
final NyDramaModel dramaOne;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _HomeRemainingSectionsState();
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeRemainingSectionsState extends State<HomeRemainingSections> {
|
||||
NyDetailModel? _detailModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_getDetailsData();
|
||||
}
|
||||
|
||||
void _getDetailsData() {
|
||||
VideoRequest.getVideoDetail(
|
||||
shortPlayId: widget.dramaOne.shortPlayId ?? 0,
|
||||
shortPlayVideoId: widget.dramaOne.shortPlayVideoId ?? 0,
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_detailModel = value;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final _ActorInfo _actor = _ActorInfo(
|
||||
name: 'Cheng Zi',
|
||||
tags: 'Romantic Lead | Revenge Drama | Lead Actor',
|
||||
bornYear: '2002',
|
||||
@ -17,164 +56,32 @@ class HomeRemainingSections extends StatelessWidget {
|
||||
nationality: 'China',
|
||||
);
|
||||
|
||||
static const List<_DramaItem> _seriesItems = [
|
||||
_DramaItem(
|
||||
title: 'Cities Of Smoke Starlight',
|
||||
category: 'Satisfying',
|
||||
palette: [Color(0xFF33241D), Color(0xFFD8A65B)],
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Cities Of Smoke Starlight',
|
||||
category: 'Satisfying',
|
||||
palette: [Color(0xFF25354C), Color(0xFFC88B57)],
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Cities Of Smoke Starlight',
|
||||
category: 'Satisfying',
|
||||
palette: [Color(0xFF3E2B2C), Color(0xFFE2B36B)],
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Cities Of Smoke Starlight',
|
||||
category: 'Satisfying',
|
||||
palette: [Color(0xFF443A2C), Color(0xFFC48645)],
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Cities Of Smoke Starlight',
|
||||
category: 'Satisfying',
|
||||
palette: [Color(0xFF314559), Color(0xFFE5AA65)],
|
||||
),
|
||||
_DramaItem(
|
||||
title: 'Cities Of Smoke Starlight',
|
||||
category: 'Satisfying',
|
||||
palette: [Color(0xFF2C2637), Color(0xFFD39458)],
|
||||
),
|
||||
];
|
||||
|
||||
static const List<String> _categoryTitles = [
|
||||
'Counterattack Life',
|
||||
'Reborn And Returned',
|
||||
'God Of War Returns',
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 92),
|
||||
child: Column(
|
||||
padding: const EdgeInsets.fromLTRB(16, 3, 16, 16),
|
||||
child: Stack(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
const _ActorSection(actor: _actor, items: _seriesItems),
|
||||
const SizedBox(height: 18),
|
||||
..._categoryTitles.map((title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 18),
|
||||
child: _DramaShelfSection(title: title, items: _seriesItems),
|
||||
);
|
||||
}),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
child: _ActorSection(actor: _actor, detailModel: _detailModel),
|
||||
),
|
||||
Positioned(
|
||||
left: -10,
|
||||
top: 0,
|
||||
child: HelpImage.asset(ConstImage.homeEyes, width: 72),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _TopRankingPoster extends StatelessWidget {
|
||||
const _TopRankingPoster({required this.item});
|
||||
|
||||
final _DramaItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: _PosterBlock(item: item, showLargeTitle: true),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
left: 5,
|
||||
child: NovyText(
|
||||
'1',
|
||||
color: ConstColor.btGreen,
|
||||
fontSize: 36,
|
||||
fontFamily: ConstFont.montBlack,
|
||||
shadow: const Shadow(
|
||||
offset: Offset(1.5, 1.5),
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
NovyText(
|
||||
item.title,
|
||||
color: Colors.black,
|
||||
fontSize: 14,
|
||||
fontFamily: ConstFont.montMedium,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
NyTagWidget(text: item.category),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RankingListItem extends StatelessWidget {
|
||||
const _RankingListItem({required this.rank, required this.item});
|
||||
|
||||
final int rank;
|
||||
final _DramaItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color rankColor = rank == 4
|
||||
? const Color(0xFFBBBBBB)
|
||||
: ConstColor.btGreen;
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(width: 42, height: 64, child: _PosterBlock(item: item)),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
NovyText(
|
||||
'$rank',
|
||||
color: rankColor,
|
||||
fontSize: 19,
|
||||
fontFamily: ConstFont.montBlack,
|
||||
lineHeight: 1,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
NovyText(
|
||||
item.title,
|
||||
color: Colors.black,
|
||||
fontSize: 11,
|
||||
fontFamily: ConstFont.montBold,
|
||||
maxLines: 1,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
NyTagWidget(text: item.category),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActorSection extends StatelessWidget {
|
||||
const _ActorSection({required this.actor, required this.items});
|
||||
const _ActorSection({required this.actor, required this.detailModel});
|
||||
|
||||
final _ActorInfo actor;
|
||||
final List<_DramaItem> items;
|
||||
final NyDetailModel? detailModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -184,26 +91,43 @@ class _ActorSection extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 18, 10, 10),
|
||||
padding: const EdgeInsets.fromLTRB(10, 16, 10, 10),
|
||||
child: Column(
|
||||
children: [
|
||||
const _CornerEyes(),
|
||||
const SizedBox(height: 4),
|
||||
_OutlinedTitle('Read This Series Of Short Plays', fontSize: 19),
|
||||
// const SizedBox(height: 10),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: OutlinedTitle(
|
||||
'Read This Series Of Short Plays',
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 26),
|
||||
const _ActorAvatar(),
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFFA6D9), Color(0xFFFFE6F5)],
|
||||
),
|
||||
),
|
||||
child: HelpImage.asset(ConstImage.homeHead, width: 100),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
NovyText(
|
||||
actor.name,
|
||||
color: Colors.black,
|
||||
fontSize: 22,
|
||||
fontSize: 18,
|
||||
fontFamily: ConstFont.montBlack,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
NovyText(
|
||||
actor.tags,
|
||||
color: const Color(0xFF888888),
|
||||
fontSize: 13,
|
||||
fontSize: 12,
|
||||
fontFamily: ConstFont.montRegular,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@ -211,9 +135,7 @@ class _ActorSection extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
_SectionHeader(title: 'Series'),
|
||||
const SizedBox(height: 8),
|
||||
_SeriesGrid(items: items, showTags: false),
|
||||
const SizedBox(height: 12),
|
||||
const _PagerDots(),
|
||||
_SeriesGrid(detailModel: detailModel),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -223,147 +145,144 @@ class _ActorSection extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DramaShelfSection extends StatelessWidget {
|
||||
const _DramaShelfSection({required this.title, required this.items});
|
||||
class _SeriesGrid extends StatefulWidget {
|
||||
const _SeriesGrid({required this.detailModel});
|
||||
|
||||
final String title;
|
||||
final List<_DramaItem> items;
|
||||
final NyDetailModel? detailModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CutCornerClipper(
|
||||
cutSize: 40,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 14, 8, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const _GripBars(),
|
||||
const SizedBox(height: 8),
|
||||
_OutlinedTitle(title, fontSize: 19),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: List.generate(3, (index) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: index == 2 ? 0 : 8),
|
||||
child: _SeriesCard(item: items[index], showTag: true),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const _GreenActionButton(text: 'Click to watch'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
State<_SeriesGrid> createState() => _SeriesGridState();
|
||||
}
|
||||
|
||||
class _SeriesGrid extends StatelessWidget {
|
||||
const _SeriesGrid({required this.items, required this.showTags});
|
||||
class _SeriesGridState extends State<_SeriesGrid> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentPage = 0;
|
||||
|
||||
final List<_DramaItem> items;
|
||||
final bool showTags;
|
||||
List<NyEpisodeModel> get _items => widget.detailModel?.episodeList ?? [];
|
||||
|
||||
String get _coverUrl {
|
||||
final shortPlayImage = widget.detailModel?.shortPlayInfo?.imageUrl ?? '';
|
||||
if (shortPlayImage.isNotEmpty) return shortPlayImage;
|
||||
return widget.detailModel?.videoInfo?.imageUrl ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _SeriesGrid oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final pageCount = episodeSeriesPageCount(_items.length);
|
||||
if (_currentPage >= pageCount) {
|
||||
_currentPage = pageCount - 1;
|
||||
if (_pageController.hasClients) {
|
||||
_pageController.jumpToPage(_currentPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: items.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 9,
|
||||
mainAxisSpacing: 10,
|
||||
childAspectRatio: 0.47,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return _SeriesCard(item: items[index], showTag: showTags);
|
||||
final pageCount = episodeSeriesPageCount(_items.length);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final gridHeight = 200.0 * 2;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: gridHeight,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: pageCount,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentPage = index;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, pageIndex) {
|
||||
final pageItems = episodeSeriesPageItems(_items, pageIndex);
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: pageItems.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 9,
|
||||
mainAxisSpacing: 10,
|
||||
childAspectRatio: 104 / 190,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = pageItems[index];
|
||||
return _SeriesCard(
|
||||
item: item,
|
||||
coverUrl: _coverUrl,
|
||||
onTap: () => _openDetail(item),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_PagerDots(count: pageCount, currentIndex: _currentPage),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _openDetail(NyEpisodeModel item) {
|
||||
final detail = widget.detailModel;
|
||||
if (detail == null) return;
|
||||
|
||||
final videos = buildEpisodeDramaList(detail);
|
||||
if (videos.isEmpty) return;
|
||||
|
||||
final selected = videos.firstWhere(
|
||||
(video) => video.shortPlayVideoId == item.shortPlayVideoId,
|
||||
orElse: () => videos.first,
|
||||
);
|
||||
AppNav.to(VideoDetailPage(drama: selected));
|
||||
}
|
||||
}
|
||||
|
||||
class _SeriesCard extends StatelessWidget {
|
||||
const _SeriesCard({required this.item, required this.showTag});
|
||||
const _SeriesCard({
|
||||
required this.item,
|
||||
required this.coverUrl,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final _DramaItem item;
|
||||
final bool showTag;
|
||||
final NyEpisodeModel item;
|
||||
final String coverUrl;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 0.68,
|
||||
child: _PosterBlock(item: item, showLargeTitle: true),
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
NovyText(
|
||||
item.title,
|
||||
color: Colors.black,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.05,
|
||||
fontFamily: ConstFont.montMedium,
|
||||
maxLines: 2,
|
||||
),
|
||||
if (showTag) ...[
|
||||
const SizedBox(height: 7),
|
||||
NyTagWidget(text: item.category),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PosterBlock extends StatelessWidget {
|
||||
const _PosterBlock({required this.item, this.showLargeTitle = false});
|
||||
|
||||
final _DramaItem item;
|
||||
final bool showLargeTitle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: item.palette,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: 10,
|
||||
right: 10,
|
||||
child: Container(
|
||||
height: showLargeTitle ? 74 : 28,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
),
|
||||
),
|
||||
AspectRatio(
|
||||
aspectRatio: 0.68,
|
||||
child: HelpImage.network(coverUrl, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(
|
||||
left: 7,
|
||||
right: 7,
|
||||
bottom: 10,
|
||||
child: NovyText(
|
||||
showLargeTitle ? 'CITIES\nOF SMOKE\nSTARLIGHT' : item.title,
|
||||
color: Colors.white,
|
||||
fontSize: showLargeTitle ? 17 : 8,
|
||||
lineHeight: 0.95,
|
||||
textAlign: TextAlign.center,
|
||||
fontFamily: ConstFont.montBlack,
|
||||
maxLines: showLargeTitle ? 3 : 2,
|
||||
shadow: const Shadow(offset: Offset(1, 1), color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
NovyText(
|
||||
'Episode ${item.episode}',
|
||||
color: Colors.black,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.05,
|
||||
fontFamily: ConstFont.montMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -371,37 +290,6 @@ class _PosterBlock extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ActorAvatar extends StatelessWidget {
|
||||
const _ActorAvatar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 116,
|
||||
height: 116,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFFA6D9), Color(0xFFFFE6F5)],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 58,
|
||||
height: 58,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: const Icon(Icons.person, size: 44, color: Color(0xFF8E2A55)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActorStats extends StatelessWidget {
|
||||
const _ActorStats({required this.actor});
|
||||
|
||||
@ -429,14 +317,14 @@ class _ActorStats extends StatelessWidget {
|
||||
NovyText(
|
||||
stats[index][0],
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
fontFamily: ConstFont.montMedium,
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
NovyText(
|
||||
stats[index][1],
|
||||
color: const Color(0xFF777777),
|
||||
fontSize: 13,
|
||||
fontSize: 12,
|
||||
fontFamily: ConstFont.montRegular,
|
||||
),
|
||||
],
|
||||
@ -467,8 +355,8 @@ class _SectionHeader extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _OutlinedTitle extends StatelessWidget {
|
||||
const _OutlinedTitle(this.text, {required this.fontSize});
|
||||
class OutlinedTitle extends StatelessWidget {
|
||||
const OutlinedTitle(this.text, {super.key, required this.fontSize});
|
||||
|
||||
final String text;
|
||||
final double fontSize;
|
||||
@ -505,151 +393,29 @@ class _OutlinedTitle extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _GreenActionButton extends StatelessWidget {
|
||||
const _GreenActionButton({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipPath(
|
||||
clipper: const EqualCutCornerClipper(
|
||||
cutSize: 18,
|
||||
corner: CutCorner.bottomLeft,
|
||||
),
|
||||
child: Container(
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: ConstColor.btGreen,
|
||||
border: Border.all(color: Colors.black, width: 2),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
const Positioned(left: 22, top: 6, child: _GripBars()),
|
||||
const Positioned(right: 10, bottom: 4, child: _GripBars()),
|
||||
Center(
|
||||
child: NovyText(
|
||||
text,
|
||||
color: Colors.black,
|
||||
fontSize: 14,
|
||||
fontFamily: ConstFont.montBlack,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CornerEyes extends StatelessWidget {
|
||||
const _CornerEyes();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 18,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
left: -3,
|
||||
top: -38,
|
||||
child: Transform.rotate(
|
||||
angle: 0.68,
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 84,
|
||||
color: ConstColor.btGreen,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Positioned(left: 18, top: -12, child: _EyeDot()),
|
||||
const Positioned(left: 38, top: -18, child: _EyeDot()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EyeDot extends StatelessWidget {
|
||||
const _EyeDot();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 22,
|
||||
height: 26,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
margin: const EdgeInsets.only(top: 2, right: 2),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PagerDots extends StatelessWidget {
|
||||
const _PagerDots();
|
||||
const _PagerDots({required this.count, required this.currentIndex});
|
||||
|
||||
final int count;
|
||||
final int currentIndex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(7, (index) {
|
||||
children: List.generate(count, (index) {
|
||||
final active = index == currentIndex;
|
||||
return Container(
|
||||
width: index == 1 ? 26 : 7,
|
||||
width: active ? 26 : 7,
|
||||
height: 7,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 3),
|
||||
color: index == 1 ? ConstColor.btGreen : const Color(0xFFD9D9D9),
|
||||
color: active ? ConstColor.btGreen : const Color(0xFFD9D9D9),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GripBars extends StatelessWidget {
|
||||
const _GripBars();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(4, (index) {
|
||||
return Container(
|
||||
width: 3,
|
||||
height: 8,
|
||||
margin: const EdgeInsets.only(right: 2),
|
||||
color: Colors.black,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DramaItem {
|
||||
const _DramaItem({
|
||||
required this.title,
|
||||
required this.category,
|
||||
required this.palette,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String category;
|
||||
final List<Color> palette;
|
||||
}
|
||||
|
||||
class _ActorInfo {
|
||||
const _ActorInfo({
|
||||
required this.name,
|
||||
@ -665,3 +431,18 @@ class _ActorInfo {
|
||||
final String height;
|
||||
final String nationality;
|
||||
}
|
||||
|
||||
int episodeSeriesPageCount(int itemCount) {
|
||||
if (itemCount <= 0) return 1;
|
||||
return (itemCount / episodeSeriesPageSize).ceil();
|
||||
}
|
||||
|
||||
List<NyEpisodeModel> episodeSeriesPageItems(
|
||||
List<NyEpisodeModel> items,
|
||||
int pageIndex,
|
||||
) {
|
||||
final start = pageIndex * episodeSeriesPageSize;
|
||||
if (start >= items.length) return [];
|
||||
final end = (start + episodeSeriesPageSize).clamp(0, items.length);
|
||||
return items.sublist(start, end);
|
||||
}
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:novyronst/root/lib_export.dart';
|
||||
import 'package:novyronst/tools/widgets/extend_widget.dart';
|
||||
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
|
||||
|
||||
class HomeSearchView extends StatefulWidget {
|
||||
const HomeSearchView({super.key,
|
||||
const HomeSearchView({
|
||||
super.key,
|
||||
this.onlyTap = false,
|
||||
this.onTap,
|
||||
this.showLogo = true,
|
||||
});
|
||||
|
||||
final bool onlyTap;
|
||||
final Function()? onTap;
|
||||
final bool showLogo;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@ -21,66 +23,92 @@ class HomeSearchView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SearchViewState extends State<HomeSearchView> {
|
||||
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: AppScreen.navBarHeight,
|
||||
padding: EdgeInsets.only(top: AppScreen.statusBarHeight,
|
||||
left: 16, right: 16,
|
||||
bottom: 5
|
||||
padding: EdgeInsets.only(
|
||||
top: AppScreen.statusBarHeight,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 5,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: Row(
|
||||
children: [
|
||||
HelpImage.asset(ConstImage.homeTopLogo, width: 36),
|
||||
const SizedBox(width: 16),
|
||||
if (widget.showLogo) ...[
|
||||
HelpImage.asset(ConstImage.homeTopLogo, width: 36),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
HelpImage.asset(ConstImage.searchBg, height: 36, fit: BoxFit.fill),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 11),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, color: Colors.white, size: 24),
|
||||
|
||||
Expanded(child:widget.onlyTap
|
||||
? NovyText('What are...... ',
|
||||
fontSize: 13,
|
||||
color: Colors.white.withAlpha(127),
|
||||
)
|
||||
: CupertinoTextField(
|
||||
controller: _searchController,
|
||||
// focusNode: widget.focusNode,
|
||||
decoration: BoxDecoration(),
|
||||
placeholder: 'What are...... ',
|
||||
placeholderStyle: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.white.withAlpha(127),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
|
||||
textInputAction: TextInputAction.search,
|
||||
onEditingComplete: () {
|
||||
|
||||
},
|
||||
),),
|
||||
|
||||
HelpImage.asset(ConstImage.homeSearch, width: 78, height: 36, fit: BoxFit.fill)
|
||||
|
||||
],
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: HelpImage.asset(
|
||||
ConstImage.searchBg,
|
||||
height: 36,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 11, right: 78),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.search,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: widget.onlyTap
|
||||
? NovyText(
|
||||
'What are...... ',
|
||||
fontSize: 13,
|
||||
color: Colors.white.withAlpha(127),
|
||||
)
|
||||
: CupertinoTextField(
|
||||
controller: _searchController,
|
||||
// focusNode: widget.focusNode,
|
||||
decoration: const BoxDecoration(),
|
||||
placeholder: 'What are...... ',
|
||||
placeholderStyle: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.white.withAlpha(127),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
|
||||
textInputAction: TextInputAction.search,
|
||||
onEditingComplete: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: HelpImage.asset(
|
||||
ConstImage.homeSearch,
|
||||
width: 78,
|
||||
height: 36,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -88,11 +116,10 @@ class _SearchViewState extends State<HomeSearchView> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 搜索页面的搜索框
|
||||
class SeSearchView extends StatefulWidget {
|
||||
const SeSearchView({super.key,
|
||||
const SeSearchView({
|
||||
super.key,
|
||||
this.onClickSearch,
|
||||
required this.controller,
|
||||
this.focusNode,
|
||||
@ -109,62 +136,77 @@ class SeSearchView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SeSearchViewState extends State<SeSearchView> {
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 44,
|
||||
padding: EdgeInsets.only(top: 4, bottom: 4),
|
||||
child: Stack(
|
||||
children: [
|
||||
HelpImage.asset(ConstImage.searchBg, height: 36, fit: BoxFit.fill),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 11),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, color: Colors.white, size: 24),
|
||||
|
||||
Expanded(child: CupertinoTextField(
|
||||
controller: widget.controller,
|
||||
focusNode: widget.focusNode,
|
||||
decoration: BoxDecoration(),
|
||||
placeholder: 'What are...... ',
|
||||
placeholderStyle: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.white.withAlpha(127),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
textInputAction: TextInputAction.search,
|
||||
onEditingComplete: () {
|
||||
if (widget.controller.text.trim().isNotEmpty) {
|
||||
widget.onClickSearch?.call(widget.controller.text);
|
||||
}
|
||||
},
|
||||
),),
|
||||
|
||||
HelpImage.asset(ConstImage.homeSearch,
|
||||
width: 78, height: 36, fit: BoxFit.fill
|
||||
).addInkWell(onTap: () {
|
||||
if (widget.controller.text.trim().isNotEmpty) {
|
||||
widget.onClickSearch?.call(widget.controller.text);
|
||||
}
|
||||
})
|
||||
|
||||
],
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4),
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: HelpImage.asset(
|
||||
ConstImage.searchBg,
|
||||
height: 36,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 11, right: 78),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.search, color: Colors.white, size: 24),
|
||||
|
||||
Expanded(
|
||||
child: CupertinoTextField(
|
||||
controller: widget.controller,
|
||||
focusNode: widget.focusNode,
|
||||
decoration: const BoxDecoration(),
|
||||
placeholder: 'What are...... ',
|
||||
placeholderStyle: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.white.withAlpha(127),
|
||||
),
|
||||
style: TextStyle(fontSize: 14, color: Colors.white),
|
||||
textInputAction: TextInputAction.search,
|
||||
onEditingComplete: () {
|
||||
if (widget.controller.text.trim().isNotEmpty) {
|
||||
widget.onClickSearch?.call(widget.controller.text);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child:
|
||||
HelpImage.asset(
|
||||
ConstImage.homeSearch,
|
||||
width: 78,
|
||||
height: 36,
|
||||
fit: BoxFit.fill,
|
||||
).addInkWell(
|
||||
onTap: () {
|
||||
if (widget.controller.text.trim().isNotEmpty) {
|
||||
widget.onClickSearch?.call(widget.controller.text);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
115
lib/pages/my/pages/about_page.dart
Normal file
115
lib/pages/my/pages/about_page.dart
Normal file
@ -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<void> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
198
lib/pages/my/pages/mine_history_page.dart
Normal file
198
lib/pages/my/pages/mine_history_page.dart
Normal file
@ -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<MineHistoryPage> createState() => _HistoryPageState();
|
||||
}
|
||||
|
||||
class _HistoryPageState extends State<MineHistoryPage> {
|
||||
List<NyDramaModel> _itemsHis = [];
|
||||
bool _isLoading = false;
|
||||
bool _isEditing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadHistory();
|
||||
}
|
||||
|
||||
Future<void> _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
298
lib/pages/my/pages/mine_link_page.dart
Normal file
298
lib/pages/my/pages/mine_link_page.dart
Normal file
@ -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<MineLinkPage> createState() => _WebLinkViewState();
|
||||
}
|
||||
|
||||
class _WebLinkViewState extends State<MineLinkPage> {
|
||||
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<Color>(
|
||||
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('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<MyPage> {
|
||||
|
||||
List<String> _titles = [
|
||||
final List<String> _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<MyPage> {
|
||||
...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<MyPage> {
|
||||
}
|
||||
|
||||
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<MyPage> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
NovyText('Visitor',
|
||||
NovyText(_name,
|
||||
fontSize: 18,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@ -121,14 +162,19 @@ class _RootPageState extends State<MyPage> {
|
||||
),
|
||||
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!');
|
||||
}),
|
||||
],
|
||||
)
|
||||
],
|
||||
|
||||
@ -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<PlayPage> {
|
||||
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<PlayPage> {
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_isLoading && _videoList.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
return const NyLoadingWidget();
|
||||
}
|
||||
|
||||
if (_videoList.isEmpty) {
|
||||
|
||||
@ -13,6 +13,8 @@ class PlayerView extends StatefulWidget {
|
||||
this.onCollectTap,
|
||||
this.isLoadingMore = false,
|
||||
this.isActive = true,
|
||||
this.bottomMetaSpacing = 76,
|
||||
this.progressHeaderBuilder,
|
||||
});
|
||||
|
||||
final List<NyDramaModel> videos;
|
||||
@ -21,6 +23,9 @@ class PlayerView extends StatefulWidget {
|
||||
final ValueChanged<int>? onCollectTap;
|
||||
final bool isLoadingMore;
|
||||
final bool isActive;
|
||||
final double bottomMetaSpacing;
|
||||
final Widget Function(BuildContext context, NyDramaModel drama, int index)?
|
||||
progressHeaderBuilder;
|
||||
|
||||
@override
|
||||
State<PlayerView> createState() => _PlayerViewState();
|
||||
@ -48,6 +53,13 @@ class _PlayerViewState extends State<PlayerView> {
|
||||
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<PlayerView> {
|
||||
}
|
||||
|
||||
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<PlayerView> {
|
||||
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<PlayerView> {
|
||||
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;
|
||||
}
|
||||
|
||||
70
lib/pages/widgets/ny_loading_widget.dart
Normal file
70
lib/pages/widgets/ny_loading_widget.dart
Normal file
@ -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<NyLoadingWidget> createState() => _ThreeDotLoadingState();
|
||||
}
|
||||
|
||||
class _ThreeDotLoadingState extends State<NyLoadingWidget>
|
||||
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'),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
|
||||
64
pubspec.lock
64
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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user