import 'dart:async'; import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/pages/dynamics/repost_dyn_panel.dart'; import 'package:PiliPlus/pages/video/detail/introduction/pay_coins_page.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/user/fav_folder.dart'; import 'package:PiliPlus/models/video/ai.dart'; import 'package:PiliPlus/models/video_detail_res.dart'; import 'package:PiliPlus/pages/video/detail/controller.dart'; import 'package:PiliPlus/pages/video/detail/reply/index.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import '../../../../http/search.dart'; import '../../../../models/model_hot_video_item.dart'; import '../related/index.dart'; class VideoIntroController extends GetxController with GetTickerProviderStateMixin { // 视频bvid late String bvid; // 是否预渲染 骨架屏 bool preRender = false; // 视频详情 上个页面传入 Map videoItem = {}; late final RxMap staffRelations = {}.obs; // 请求状态 RxBool isLoading = false.obs; // 视频详情 请求返回 Rx videoDetail = VideoDetailData().obs; // up主粉丝数 Rx> userStat = Rx>({'follower': '-'}); dynamic videoTags; // 是否点赞 RxBool hasLike = false.obs; // 是否点踩 RxBool hasDislike = false.obs; // 投币数量 final RxInt _coinNum = 0.obs; // 是否投币 bool get hasCoin => _coinNum.value != 0; // 是否收藏 RxBool hasFav = false.obs; // 是否稍后再看 RxBool hasLater = false.obs; bool isLogin = false; Rx favFolderData = FavFolderData().obs; Set? favIds; // 关注状态 默认未关注 RxMap followStatus = {}.obs; RxInt lastPlayCid = 0.obs; dynamic userInfo; // 同时观看 bool isShowOnlineTotal = false; RxString total = '1'.obs; Timer? timer; String heroTag = ''; late ModelResult modelResult; Rx> queryVideoIntroData = Rx>({"status": true}); ExpandableController? expandableCtr; late final showArgueMsg = GStorage.showArgueMsg; late final enableAi = GStorage.setting.get(SettingBoxKey.enableAi, defaultValue: false); late final enableQuickFav = GStorage.setting.get(SettingBoxKey.enableQuickFav, defaultValue: false); @override void onInit() { super.onInit(); userInfo = GStorage.userInfo.get('userInfoCache'); try { if (heroTag.isEmpty) { heroTag = Get.arguments['heroTag']; } bvid = Get.parameters['bvid']!; } catch (_) {} if (Get.arguments.isNotEmpty) { if (Get.arguments.containsKey('videoItem')) { preRender = true; var args = Get.arguments['videoItem']; var keys = Get.arguments.keys.toList(); try { if (args.pic != null && args.pic != '') { videoItem['pic'] = args.pic; } else if (args.cover != null && args.cover != '') { videoItem['pic'] = args.cover; } } catch (_) {} if (args.title is String) { videoItem['title'] = args.title; } else { String str = ''; for (Map map in args.title) { str += map['text']; } videoItem['title'] = str; } videoItem['stat'] = keys.contains('stat') ? args.stat : null; videoItem['pubdate'] = keys.contains('pubdate') ? args.pubdate : null; videoItem['owner'] = keys.contains('owner') ? args.owner : null; } } isLogin = userInfo != null; lastPlayCid.value = int.parse(Get.parameters['cid']!); isShowOnlineTotal = GStorage.setting .get(SettingBoxKey.enableOnlineTotal, defaultValue: false); startTimer(); queryVideoIntro(); } // 获取视频简介&分p Future queryVideoIntro() async { queryVideoTags(); var result = await VideoHttp.videoIntro(bvid: bvid); if (result['status']) { VideoDetailData data = result['data']; if (videoDetail.value.ugcSeason?.id == data.ugcSeason?.id) { // keep reversed season data.ugcSeason = videoDetail.value.ugcSeason; } if (videoDetail.value.cid == data.cid) { // keep reversed pages data.pages = videoDetail.value.pages; data.isPageReversed = videoDetail.value.isPageReversed; } videoDetail.value = data; videoItem['staff'] = data.staff; try { final videoDetailController = Get.find(tag: heroTag); if (videoDetailController.videoItem['pic'] == null || videoDetailController.videoItem['pic'] == '' || (videoDetailController.videoUrl.isNullOrEmpty && videoDetailController.isQuerying.not)) { videoDetailController.videoItem['pic'] = data.pic; } if (videoDetailController.showReply) { try { final videoReplyController = Get.find(tag: heroTag); videoReplyController.count.value = data.stat?.reply ?? 0; } catch (_) {} } } catch (_) {} if (videoDetail.value.pages != null && videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) { lastPlayCid.value = videoDetail.value.pages!.first.cid!; } // Get.find(tag: heroTag).tabs.value = [ // '简介', // '评论 ${result['data']!.stat!.reply}' // ]; // 获取到粉丝数再返回 queryUserStat(); } else { SmartDialog.showToast( "${result['code']} ${result['msg']} ${result['data']}"); } queryVideoIntroData.value = result; if (isLogin) { queryAllStatus(); queryFollowStatus(); } } Future queryVideoTags() async { var result = await UserHttp.videoTags(bvid: bvid); if (result['status']) { videoTags = result['data']; } } // 获取up主粉丝数 Future queryUserStat() async { if (videoItem['staff']?.isNotEmpty == true) { Request().get( Api.relations, queryParameters: { 'fids': (videoItem['staff'] as List) .map((item) => item.mid) .join(',') }, ).then((res) { if (res.data['code'] == 0) { staffRelations.value = { 'status': true, if (res.data['data'] != null) ...res.data['data'], }; } }); } else { if (videoDetail.value.owner == null) { return; } var result = await MemberHttp.memberCardInfo(mid: videoDetail.value.owner!.mid!); if (result['status']) { userStat.value = result['data']; } } } Future queryAllStatus() async { var result = await VideoHttp.videoRelation(bvid: bvid); if (result['status']) { var data = result['data']; hasLike.value = data['like']; hasDislike.value = data['dislike']; _coinNum.value = data['coin']; hasFav.value = data['favorite']; } } // 一键三连 Future actionOneThree() async { feedBack(); if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } if (hasLike.value && hasCoin && hasFav.value) { // 已点赞、投币、收藏 SmartDialog.showToast('已三连'); return false; } var result = await VideoHttp.oneThree(bvid: bvid); if (result['status']) { hasLike.value = result["data"]["like"]; if (result["data"]["coin"]) { _coinNum.value = 2; GlobalData().afterCoin(2); } hasFav.value = result["data"]["fav"]; SmartDialog.showToast('三连成功'); } else { SmartDialog.showToast(result['msg']); } } // (取消)点赞 Future actionLikeVideo() async { if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } if (videoDetail.value.stat?.like == null) { // not init return; } var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value); if (result['status']) { // hasLike.value = result["data"] == 1 ? true : false; if (!hasLike.value) { SmartDialog.showToast(result['data']['toast']); hasLike.value = true; hasDislike.value = false; videoDetail.value.stat!.like = videoDetail.value.stat!.like! + 1; } else if (hasLike.value) { SmartDialog.showToast('取消赞'); hasLike.value = false; videoDetail.value.stat!.like = videoDetail.value.stat!.like! - 1; } } else { SmartDialog.showToast(result['msg']); } } Future actionDislikeVideo() async { if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } var result = await VideoHttp.dislikeVideo(bvid: bvid, type: !hasDislike.value); if (result['status']) { // hasLike.value = result["data"] == 1 ? true : false; if (!hasDislike.value) { SmartDialog.showToast('点踩成功'); hasDislike.value = true; hasLike.value = false; } else { SmartDialog.showToast('取消踩'); hasDislike.value = false; } // hasDislike.refresh(); } else { SmartDialog.showToast(result['msg']); } } Future viewLater() async { var res = await (hasLater.value ? UserHttp.toViewDel(aids: [IdUtils.bv2av(bvid)]) : await UserHttp.toViewLater(bvid: bvid)); if (res['status']) hasLater.value = !hasLater.value; SmartDialog.showToast(res['msg']); } void coinVideo(int coin, [bool selectLike = false]) async { if (videoDetail.value.stat?.coin == null) { // not init return; } var res = await VideoHttp.coinVideo( bvid: bvid, multiply: coin, selectLike: selectLike ? 1 : 0, ); if (res['status']) { SmartDialog.showToast('投币成功'); _coinNum.value += coin; GlobalData().afterCoin(coin); videoDetail.value.stat!.coin = videoDetail.value.stat!.coin! + coin; if (selectLike && hasLike.value.not) { hasLike.value = true; videoDetail.value.stat!.like = videoDetail.value.stat!.like! + 1; } } else { SmartDialog.showToast(res['msg']); } } // 投币 Future actionCoinVideo() async { if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } int copyright = (queryVideoIntroData.value['data'] as VideoDetailData?)?.copyright ?? 1; if ((copyright != 1 && _coinNum.value >= 1) || _coinNum.value >= 2) { SmartDialog.showToast('达到投币上限啦~'); return; } if (GlobalData().coins != null && GlobalData().coins! < 1) { SmartDialog.showToast('硬币不足'); return; } PayCoinsPage.toPayCoinsPage( onPayCoin: coinVideo, copyright: copyright, hasCoin: _coinNum.value == 1, ); } // (取消)收藏 Future actionFavVideo({type = 'choose'}) async { // 收藏至默认文件夹 if (type == 'default') { SmartDialog.showLoading(msg: '请求中'); queryVideoInFolder().then((res) async { if (res['status']) { int defaultFolderId = favFolderData.value.list!.first.id!; bool notInDefFolder = favFolderData.value.list!.first.favState! == 0; var result = await VideoHttp.favVideo( aid: IdUtils.bv2av(bvid), addIds: notInDefFolder ? '$defaultFolderId' : '', delIds: !notInDefFolder ? '$defaultFolderId' : '', ); SmartDialog.dismiss(); if (result['status']) { hasFav.value = !hasFav.value || (hasFav.value && notInDefFolder); // 重新获取收藏状态 // await queryHasFavVideo(); SmartDialog.showToast('✅ 快速收藏/取消收藏成功'); } else { SmartDialog.showToast(result['msg']); } } else { SmartDialog.dismiss(); } }); return; } List addMediaIdsNew = []; List delMediaIdsNew = []; try { for (var i in favFolderData.value.list!.toList()) { bool isFaved = favIds?.contains(i.id) == true; if (i.favState == 1) { if (isFaved.not) { addMediaIdsNew.add(i.id); } } else { if (isFaved) { delMediaIdsNew.add(i.id); } } } } catch (e) { debugPrint(e.toString()); } SmartDialog.showLoading(msg: '请求中'); var result = await VideoHttp.favVideo( aid: IdUtils.bv2av(bvid), addIds: addMediaIdsNew.join(','), delIds: delMediaIdsNew.join(','), ); SmartDialog.dismiss(); if (result['status']) { Get.back(); hasFav.value = addMediaIdsNew.isNotEmpty || favIds?.length != delMediaIdsNew.length; // 重新获取收藏状态 // await queryHasFavVideo(); SmartDialog.showToast('操作成功'); } else { SmartDialog.showToast(result['msg']); } } // 分享视频 Future actionShareVideo(context) async { showDialog( context: context, builder: (context) { String videoUrl = '${HttpString.baseUrl}/video/$bvid'; return AlertDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), content: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( title: const Text( '复制链接', style: TextStyle(fontSize: 14), ), onTap: () { Get.back(); Utils.copyText(videoUrl); }, ), ListTile( title: const Text( '其它app打开', style: TextStyle(fontSize: 14), ), onTap: () { Get.back(); Utils.launchURL(videoUrl); }, ), ListTile( title: const Text( '分享视频', style: TextStyle(fontSize: 14), ), onTap: () { Get.back(); Utils.shareText('${videoDetail.value.title} ' 'UP主: ${videoDetail.value.owner!.name!}' ' - $videoUrl'); }, ), ListTile( title: const Text( '分享至动态', style: TextStyle(fontSize: 14), ), onTap: () { Get.back(); showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, builder: (context) => RepostPanel( rid: videoDetail.value.aid, dynType: 8, pic: videoDetail.value.pic, title: videoDetail.value.title, uname: videoDetail.value.owner?.name, ), ); }, ), ], ), ); }); } Future queryVideoInFolder() async { favIds = null; var result = await VideoHttp.videoInFolder( mid: userInfo.mid, rid: IdUtils.bv2av(bvid)); if (result['status']) { favFolderData.value = result['data']; favIds = favFolderData.value.list ?.where((item) => item.favState == 1) .map((item) => item.id) .toSet(); } return result; } // 选择文件夹 onChoose(bool checkValue, int index) { feedBack(); List datalist = favFolderData.value.list!; datalist[index].favState = checkValue ? 1 : 0; datalist[index].mediaCount = checkValue ? datalist[index].mediaCount! + 1 : datalist[index].mediaCount! - 1; favFolderData.value.list = datalist; favFolderData.refresh(); } // 查询关注状态 Future queryFollowStatus() async { if (videoDetail.value.owner == null) { return; } var result = await UserHttp.hasFollow(videoDetail.value.owner!.mid!); if (result['status']) { followStatus.value = result['data']; } } // 关注/取关up Future actionRelationMod(BuildContext context) async { if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } int attr = followStatus['attribute'] ?? 0; if (attr == 128) { dynamic res = await VideoHttp.relationMod( mid: videoDetail.value.owner?.mid ?? -1, act: 6, reSrc: 11, ); if (res['status']) { followStatus['attribute'] = 0; } return; } else { Utils.actionRelationMod( context: context, mid: videoDetail.value.owner?.mid, isFollow: attr != 0, followStatus: followStatus, callback: (attribute) { followStatus['attribute'] = attribute; }, ); } // MemberController _ = Get.put(MemberController(mid: mid), // tag: mid.toString()); // await _.getInfo(); // if (context.mounted) await _.actionRelationMod(context); // followStatus['attribute'] = _.attribute.value; // followStatus.refresh(); // Get.delete(tag: mid.toString()); } // 修改分P或番剧分集 bool changeSeasonOrbangu(epid, bvid, cid, aid, cover, [isStein]) { // 重新获取视频资源 final videoDetailCtr = Get.find(tag: heroTag); if (videoDetailCtr.isPlayAll) { if (videoDetailCtr.mediaList.indexWhere((item) => item.bvid == bvid) == -1) { Utils.toViewPage( 'bvid=$bvid&cid=$cid', arguments: { if (cover != null) 'pic': cover, 'heroTag': Utils.makeHeroTag(bvid), }, ); return false; } } videoDetailCtr ..plPlayerController.pause() ..makeHeartBeat() ..updateMediaListHistory(aid) ..onReset(isStein) ..bvid = bvid ..oid.value = aid ?? IdUtils.bv2av(bvid) ..cid.value = cid ..danmakuCid.value = cid ..queryVideoUrl(); if (this.bvid != bvid) { if (cover is String && cover.isNotEmpty) { videoDetailCtr.videoItem['pic'] = cover; } // 重新请求相关视频 if (videoDetailCtr.showRelatedVideo) { try { Get.find(tag: heroTag) ..bvid = bvid ..queryData(); } catch (_) {} } // 重新请求评论 if (videoDetailCtr.showReply) { try { Get.find(tag: heroTag) ..aid = aid ..onReload(); } catch (_) {} } hasLater.value = false; this.bvid = bvid; queryVideoIntro(); } lastPlayCid.value = cid; queryOnlineTotal(); return true; } void startTimer() { if (isShowOnlineTotal) { queryOnlineTotal(); timer ??= Timer.periodic(const Duration(seconds: 10), (Timer timer) { queryOnlineTotal(); }); } } void canelTimer() { timer?.cancel(); timer = null; } // 查看同时在看人数 Future queryOnlineTotal() async { if (isShowOnlineTotal.not) { return; } dynamic result = await VideoHttp.onlineTotal( aid: IdUtils.bv2av(bvid), bvid: bvid, cid: lastPlayCid.value, ); // dynamic result = await GrpcRepo.playerOnline( // aid: IdUtils.bv2av(bvid), // cid: lastPlayCid.value, // ); if (result['status']) { total.value = result['data']; } } @override void onClose() { canelTimer(); expandableCtr?.dispose(); expandableCtr = null; super.onClose(); } /// 播放上一个 bool prevPlay([bool skipPages = false]) { final List episodes = []; bool isPages = false; final videoDetailCtr = Get.find(tag: heroTag); if (skipPages.not && (videoDetail.value.pages?.length ?? 0) > 1) { isPages = true; final List pages = videoDetail.value.pages!; episodes.addAll(pages); } else if (videoDetailCtr.isPlayAll) { episodes.addAll(videoDetailCtr.mediaList); } else if (videoDetail.value.ugcSeason != null) { final UgcSeason ugcSeason = videoDetail.value.ugcSeason!; final List sections = ugcSeason.sections!; for (int i = 0; i < sections.length; i++) { final List episodesList = sections[i].episodes!; episodes.addAll(episodesList); } } final int currentIndex = episodes.indexWhere((e) => e.cid == (skipPages ? videoDetail.value.isPageReversed == true ? videoDetail.value.pages!.last.cid : videoDetail.value.pages!.first.cid : lastPlayCid.value)); int prevIndex = currentIndex - 1; final PlayRepeat playRepeat = videoDetailCtr.plPlayerController.playRepeat; // 列表循环 if (prevIndex < 0) { if (isPages && (videoDetailCtr.isPlayAll || videoDetail.value.ugcSeason != null)) { return prevPlay(true); } if (playRepeat == PlayRepeat.listCycle) { prevIndex = episodes.length - 1; } else { return false; } } final int cid = episodes[prevIndex].cid!; final String rBvid = isPages ? bvid : episodes[prevIndex].bvid; final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[prevIndex].aid!; changeSeasonOrbangu(null, rBvid, cid, rAid, null); return true; } /// 列表循环或者顺序播放时,自动播放下一个 bool nextPlay([bool skipPages = false]) { try { final List episodes = []; bool isPages = false; final videoDetailCtr = Get.find(tag: heroTag); // part -> playall -> season if (skipPages.not && (videoDetail.value.pages?.length ?? 0) > 1) { isPages = true; final List pages = videoDetail.value.pages!; episodes.addAll(pages); } else if (videoDetailCtr.isPlayAll) { episodes.addAll(videoDetailCtr.mediaList); } else if (videoDetail.value.ugcSeason != null) { final UgcSeason ugcSeason = videoDetail.value.ugcSeason!; final List sections = ugcSeason.sections!; for (int i = 0; i < sections.length; i++) { final List episodesList = sections[i].episodes!; episodes.addAll(episodesList); } } final PlayRepeat playRepeat = videoDetailCtr.plPlayerController.playRepeat; if (episodes.isEmpty) { if (playRepeat == PlayRepeat.autoPlayRelated && videoDetailCtr.showRelatedVideo) { return playRelated(); } return false; } final int currentIndex = episodes.indexWhere((e) => e.cid == (skipPages ? videoDetail.value.isPageReversed == true ? videoDetail.value.pages!.last.cid : videoDetail.value.pages!.first.cid : videoDetailCtr.cid.value)); int nextIndex = currentIndex + 1; if (isPages.not && videoDetailCtr.isPlayAll && currentIndex == episodes.length - 2) { videoDetailCtr.getMediaList(); } // 列表循环 if (nextIndex >= episodes.length) { if (isPages && (videoDetailCtr.isPlayAll || videoDetail.value.ugcSeason != null)) { return nextPlay(true); } if (playRepeat == PlayRepeat.listCycle) { nextIndex = 0; } else if (playRepeat == PlayRepeat.autoPlayRelated && videoDetailCtr.showRelatedVideo) { return playRelated(); } else { return false; } } int cid = episodes[nextIndex].cid!; while (cid == -1) { SmartDialog.showToast('当前视频暂不支持播放,自动跳过'); nextIndex++; if (nextIndex >= episodes.length) { return false; } cid = episodes[nextIndex].cid!; } final String rBvid = isPages ? bvid : episodes[nextIndex].bvid; final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!; changeSeasonOrbangu(null, rBvid, cid, rAid, null); return true; } catch (_) { return false; } } bool playRelated() { late RelatedController relatedCtr; try { relatedCtr = Get.find(tag: heroTag); if (relatedCtr.loadingState.value is! Success) { return false; } if ((relatedCtr.loadingState.value as Success).response.isEmpty == true) { SmartDialog.showToast('暂无相关视频,停止连播'); return false; } } catch (_) { relatedCtr = Get.put(RelatedController(), tag: heroTag); relatedCtr.queryData().then((value) { if (value['status']) { playRelated(); } }); return false; } final HotVideoItemModel videoItem = (relatedCtr.loadingState.value as Success).response[0]; try { if (videoItem.cid != null) { changeSeasonOrbangu( null, videoItem.bvid, videoItem.cid, videoItem.aid, videoItem.pic, ); } else { SearchHttp.ab2c(aid: videoItem.aid, bvid: videoItem.bvid).then( (cid) => Utils.toViewPage( 'bvid=${videoItem.bvid}&cid=${videoItem.cid}', arguments: { 'videoItem': videoItem, 'heroTag': heroTag, }, off: true, ), ); } } catch (err) { SmartDialog.showToast(err.toString()); } return true; } // ai总结 Future aiConclusion() async { SmartDialog.showLoading(msg: '正在获取AI总结'); final res = await VideoHttp.aiConclusion( bvid: bvid, cid: lastPlayCid.value, upMid: videoDetail.value.owner?.mid, ); SmartDialog.dismiss(); if (res['status']) { modelResult = res['data'].modelResult; } else { SmartDialog.showToast("当前视频可能暂不支持AI视频总结"); } return res; } // 收藏 showFavBottomSheet(BuildContext context, {type = 'tap'}) { if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } // 快速收藏 & // 点按 收藏至默认文件夹 // 长按选择文件夹 if (enableQuickFav) { if (type == 'tap') { actionFavVideo(type: 'default'); } else { Utils.showFavBottomSheet(context: context, ctr: this); } } else if (type != 'longPress') { Utils.showFavBottomSheet(context: context, ctr: this); } } }