novyronst/lib/pages/first/player_detail/video_detail_page.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;
}