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'; import 'package:novyronst/pages/widgets/ny_loading_widget.dart'; class VideoDetailPage extends StatefulWidget { const VideoDetailPage({super.key, required this.drama}); final NyDramaModel drama; @override State createState() => _VideoDetailPageState(); } class _VideoDetailPageState extends State { NyDetailModel? _detail; List _videos = []; int _currentIndex = 0; bool _isLoading = false; @override void initState() { super.initState(); _loadDetail(); } Future _loadDetail() async { setState(() { _isLoading = true; }); // id: 3150, short_id: 1913 short_play_video_id: 207599 short_play_id: 3150 final detail = await VideoRequest.getVideoDetail( shortPlayId: widget.drama.shortPlayId ?? 0, shortPlayVideoId: widget.drama.shortPlayVideoId ?? 0 ); if (!mounted) return; final videos = detail == null ? [widget.drama] : buildEpisodeDramaList(detail); final nextIndex = detail == null ? 0 : episodeIndexForDrama(detail.episodeList, widget.drama); setState(() { _detail = detail; _videos = videos; _currentIndex = nextIndex < 0 ? 0 : nextIndex; _isLoading = false; }); } Future _toggleCollect(int index) async { if (index < 0 || index >= _videos.length) return; final item = _videos[index]; final videoId = item.shortPlayVideoId ?? item.id; if (videoId == null) return; final shortPlayId = item.shortPlayId ?? item.shortId; final oldIsCollect = item.isCollect ?? false; final oldCollectTotal = item.collectTotal; final newIsCollect = !oldIsCollect; final newCollectTotal = oldCollectTotal + (newIsCollect ? 1 : -1); setState(() { _videos[index] = item.copyWith( isCollect: newIsCollect, collectTotal: newCollectTotal < 0 ? 0 : newCollectTotal, ); }); final success = newIsCollect ? await VideoRequest.collect(shortPlayId: shortPlayId, videoId: videoId) : await VideoRequest.cancelCollect(shortPlayId: shortPlayId); if (!mounted || success) return; setState(() { _videos[index] = _videos[index].copyWith( isCollect: oldIsCollect, collectTotal: oldCollectTotal, ); }); } void _showEpisodeSheet() { final detail = _detail; if (detail == null || detail.episodeList.isEmpty) return; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) { return VideoEspPage( detail: detail, currentEpisode: _currentEpisode, onSelectedEpisode: (episode) { Navigator.pop(context); final nextIndex = episodeIndexForEpisode( detail.episodeList, episode, ); if (nextIndex < 0) return; setState(() { _currentIndex = nextIndex; }); }, ); }, ); } int get _currentEpisode { if (_videos.isEmpty || _currentIndex >= _videos.length) return 1; return _videos[_currentIndex].episode ?? _currentIndex + 1; } int get _totalEpisode { final detail = _detail; final total = detail?.shortPlayInfo?.episodeTotal ?? widget.drama.episodeTotal; if (total > 0) return total; return detail?.episodeList.length ?? _videos.length; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ Positioned.fill(child: _buildBody()), Positioned( left: 10, top: MediaQuery.paddingOf(context).top + 8, child: IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white), ), ), ], ), ); } Widget _buildBody() { if (_isLoading && _videos.isEmpty) { return const NyLoadingWidget(); } if (_videos.isEmpty) { return const Center( child: Text('No videos', style: TextStyle(color: Colors.white70)), ); } return PlayerView( videos: _videos, initialIndex: _currentIndex, bottomMetaSpacing: 24, onPageChanged: (index) { setState(() { _currentIndex = index; }); }, onCollectTap: _toggleCollect, progressHeaderBuilder: (context, drama, index) { return _EpisodeSelectorBar( currentEpisode: drama.episode ?? index + 1, totalEpisode: _totalEpisode, onTap: _showEpisodeSheet, ); }, ); } } class _EpisodeSelectorBar extends StatelessWidget { const _EpisodeSelectorBar({ required this.currentEpisode, required this.totalEpisode, required this.onTap, }); final int currentEpisode; final int totalEpisode; final VoidCallback onTap; @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Container( height: 38, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(4), ), child: Row( children: [ const Icon( Icons.video_library_outlined, color: Colors.white, size: 17, ), const SizedBox(width: 10), Text( 'EP.$currentEpisode', style: const TextStyle(color: Colors.white, fontSize: 13), ), Text( ' / EP.$totalEpisode', style: TextStyle( color: Colors.white.withValues(alpha: 0.5), fontSize: 13, ), ), const Spacer(), const Icon(Icons.arrow_forward_ios, color: Colors.white, size: 13), ], ), ), ); } } List buildEpisodeDramaList(NyDetailModel detail) { final drama = detail.shortPlayInfo ?? detail.videoInfo; final info = detail.videoInfo ?? detail.shortPlayInfo; final total = drama?.episodeTotal ?? detail.episodeList.length; return detail.episodeList.map((episode) { return NyDramaModel( id: episode.id, shortId: episode.shortId, shortPlayId: episode.shortPlayId, name: drama?.name ?? info?.name ?? '', description: drama?.description ?? info?.description ?? '', imageUrl: drama?.imageUrl ?? info?.imageUrl ?? '', horizontallyImg: drama?.horizontallyImg ?? info?.horizontallyImg ?? '', collectTotal: drama?.collectTotal ?? info?.collectTotal ?? 0, watchTotal: drama?.watchTotal ?? info?.watchTotal ?? 0, episodeTotal: total, isCollect: detail.isCollect ?? drama?.isCollect ?? info?.isCollect, videoUrl: episode.videoUrl, videoInfo: {'video_url': episode.videoUrl}, tagType: drama?.tagType ?? info?.tagType ?? '', episode: episode.episode, shortPlayVideoId: episode.shortPlayVideoId, currentEpisode: episode.episode, ); }).toList(); } int episodeIndexForEpisode( List episodes, NyEpisodeModel selectedEpisode, ) { return episodes.indexWhere( (episode) => episode.shortPlayVideoId == selectedEpisode.shortPlayVideoId, ); } int episodeIndexForDrama(List episodes, NyDramaModel drama) { final videoId = drama.shortPlayVideoId ?? drama.id; if (videoId != null) { final index = episodes.indexWhere( (episode) => episode.shortPlayVideoId == videoId, ); if (index >= 0) return index; } final episodeNumber = drama.episode ?? drama.currentEpisode; if (episodeNumber != null) { return episodes.indexWhere((episode) => episode.episode == episodeNumber); } return -1; }