292 lines
8.3 KiB
Dart
292 lines
8.3 KiB
Dart
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;
|
|
}
|