Files
PiliPlus/lib/pages/video/introduction/ugc/controller.dart
Gujial 511ff71f5f opt share video link (#1835)
* feat: share video with current time position preference

* simplify


---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-14 21:16:47 +08:00

789 lines
23 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:math';
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/http/search.dart';
import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/common/video/source_type.dart';
import 'package:PiliPlus/models_new/member_card_info/data.dart';
import 'package:PiliPlus/models_new/video/video_ai_conclusion/model_result.dart';
import 'package:PiliPlus/models_new/video/video_detail/episode.dart';
import 'package:PiliPlus/models_new/video/video_detail/page.dart';
import 'package:PiliPlus/models_new/video/video_detail/section.dart';
import 'package:PiliPlus/models_new/video/video_detail/staff.dart';
import 'package:PiliPlus/models_new/video/video_detail/stat_detail.dart';
import 'package:PiliPlus/models_new/video/video_detail/ugc_season.dart';
import 'package:PiliPlus/pages/common/common_intro_controller.dart';
import 'package:PiliPlus/pages/dynamics_repost/view.dart';
import 'package:PiliPlus/pages/video/pay_coins/view.dart';
import 'package:PiliPlus/pages/video/related/controller.dart';
import 'package:PiliPlus/pages/video/reply/controller.dart';
import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart';
import 'package:PiliPlus/services/service_locator.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/extension/context_ext.dart';
import 'package:PiliPlus/utils/extension/string_ext.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:PiliPlus/utils/request_utils.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:expandable/expandable.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class UgcIntroController extends CommonIntroController with ReloadMixin {
late ExpandableController expandableCtr;
final RxBool status = true.obs;
// up主粉丝数
final Rx<MemberCardInfoData> userStat = MemberCardInfoData().obs;
// 关注状态 默认未关注
late final RxMap followStatus = {}.obs;
late final RxMap staffRelations = {}.obs;
// 是否点踩
final RxBool hasDislike = false.obs;
late final showArgueMsg = Pref.showArgueMsg;
late final enableAi = Pref.enableAi;
late final horizontalMemberPage = Pref.horizontalMemberPage;
AiConclusionResult? aiConclusionResult;
late final Map<int?, bool> seasonFavState = {};
@override
void onInit() {
super.onInit();
bool alwaysExpandIntroPanel = Pref.alwaysExpandIntroPanel;
expandableCtr = ExpandableController(
initialExpanded: alwaysExpandIntroPanel,
);
if (!alwaysExpandIntroPanel && Pref.expandIntroPanelH) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!expandableCtr.expanded && Get.context!.isLandscape) {
expandableCtr.toggle();
}
});
}
videoDetail.value.title = Get.arguments['title'] ?? '';
}
// 获取视频简介&分p
@override
Future<void> queryVideoIntro() async {
queryVideoTags();
final res = await VideoHttp.videoIntro(bvid: bvid);
if (res case Success(:final response)) {
videoPlayerServiceHandler?.onVideoDetailChange(
response,
cid.value,
heroTag,
);
if (videoDetail.value.ugcSeason?.id == response.ugcSeason?.id) {
// keep reversed season
response.ugcSeason = videoDetail.value.ugcSeason;
}
if (videoDetail.value.cid == response.cid) {
// keep reversed pages
response
..pages = videoDetail.value.pages
..isPageReversed = videoDetail.value.isPageReversed;
}
videoDetail.value = response;
try {
if (videoDetailCtr.cover.value.isEmpty ||
(videoDetailCtr.videoUrl.isNullOrEmpty &&
!videoDetailCtr.isQuerying)) {
videoDetailCtr.cover.value = response.pic ?? '';
}
if (videoDetailCtr.showReply) {
try {
Get.find<VideoReplyController>(tag: heroTag).count.value =
response.stat?.reply ?? 0;
} catch (_) {}
}
} catch (_) {}
final pages = videoDetail.value.pages;
if (pages != null && pages.isNotEmpty && cid.value == 0) {
cid.value = pages.first.cid!;
}
queryUserStat(response.staff);
} else {
res.toast();
status.value = false;
}
if (isLogin) {
queryAllStatus();
queryFollowStatus();
}
}
// 获取up主粉丝数
Future<void> queryUserStat(List<Staff>? staff) async {
if (staff != null && staff.isNotEmpty) {
final res = await Request().get(
Api.relations,
queryParameters: {'fids': staff.map((item) => item.mid).join(',')},
);
if (res.data['code'] == 0) {
staffRelations.addAll({'status': true, ...?res.data['data']});
}
} else {
final mid = videoDetail.value.owner?.mid;
if (mid == null) {
return;
}
final res = await MemberHttp.memberCardInfo(mid: mid);
if (res case Success(:final response)) {
userStat.value = response;
}
}
}
Future<void> queryAllStatus() async {
final result = await VideoHttp.videoRelation(bvid: bvid);
if (result case Success(:final response)) {
late final stat = videoDetail.value.stat;
if (response.like!) {
stat?.like = max(1, stat.like);
}
if (response.favorite!) {
stat?.favorite = max(1, stat.favorite);
}
hasLike.value = response.like!;
hasDislike.value = response.dislike!;
coinNum.value = response.coin!;
hasFav.value = response.favorite!;
}
}
// 一键三连
@override
Future<void> actionTriple() async {
feedBack();
if (!isLogin) {
SmartDialog.showToast('账号未登录');
return;
}
if (hasLike.value && hasCoin && hasFav.value) {
// 已点赞、投币、收藏
SmartDialog.showToast('已三连');
return;
}
final result = await VideoHttp.ugcTriple(bvid: bvid);
if (result case Success(:final response)) {
late final stat = videoDetail.value.stat;
if (response.like == true && !hasLike.value) {
stat?.like++;
hasLike.value = true;
}
if (response.coin == true && !hasCoin) {
stat?.coin += 2;
coinNum.value = 2;
GlobalData().afterCoin(2);
}
if (response.fav == true && !hasFav.value) {
stat?.favorite++;
hasFav.value = true;
}
hasDislike.value = false;
if (!hasCoin) {
SmartDialog.showToast('投币失败');
} else {
SmartDialog.showToast('三连成功');
}
} else {
result.toast();
}
}
// (取消)点赞
@override
Future<void> actionLikeVideo() async {
if (!isLogin) {
SmartDialog.showToast('账号未登录');
return;
}
if (videoDetail.value.stat == null) {
return;
}
final newVal = !hasLike.value;
final result = await VideoHttp.likeVideo(bvid: bvid, type: newVal);
if (result case Success(:final response)) {
SmartDialog.showToast(newVal ? response : '取消赞');
videoDetail.value.stat?.like += newVal ? 1 : -1;
hasLike.value = newVal;
if (newVal) {
hasDislike.value = false;
}
} else {
result.toast();
}
}
Future<void> actionDislikeVideo() async {
if (!isLogin) {
SmartDialog.showToast('账号未登录');
return;
}
final res = await VideoHttp.dislikeVideo(
bvid: bvid,
type: !hasDislike.value,
);
if (res.isSuccess) {
if (!hasDislike.value) {
SmartDialog.showToast('点踩成功');
hasDislike.value = true;
if (hasLike.value) {
videoDetail.value.stat?.like--;
hasLike.value = false;
}
} else {
SmartDialog.showToast('取消踩');
hasDislike.value = false;
}
} else {
res.toast();
}
}
// 投币
@override
void actionCoinVideo() {
if (!isLogin) {
SmartDialog.showToast('账号未登录');
return;
}
int copyright = videoDetail.value.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,
);
}
@override
(Object, int) get getFavRidType => (IdUtils.bv2av(bvid), 2);
@override
StatDetail? getStat() => videoDetail.value.stat;
// 分享视频
@override
void actionShareVideo(BuildContext context) {
final videoDetail = this.videoDetail.value;
final playedTimePos = videoDetailCtr.playedTimePos;
String videoUrl = '${HttpString.baseUrl}/video/$bvid';
showDialog(
context: context,
builder: (_) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
dense: true,
title: const Text(
'复制链接',
style: TextStyle(fontSize: 14),
),
onTap: () {
Get.back();
Utils.copyText(videoUrl);
},
trailing: playedTimePos.isNotEmpty
? iconButton(
tooltip: '精确分享',
icon: const Icon(Icons.timer_outlined),
onPressed: () {
Get.back();
Utils.copyText('$videoUrl$playedTimePos');
},
)
: null,
),
ListTile(
dense: true,
title: const Text(
'其它app打开',
style: TextStyle(fontSize: 14),
),
onTap: () {
Get.back();
PageUtils.launchURL(videoUrl);
},
),
if (PlatformUtils.isMobile)
ListTile(
dense: true,
title: const Text(
'分享视频',
style: TextStyle(fontSize: 14),
),
onTap: () {
Get.back();
Utils.shareText(
'${videoDetail.title} '
'UP主: ${videoDetail.owner!.name!}'
' - $videoUrl',
);
},
),
ListTile(
dense: true,
title: const Text(
'分享至动态',
style: TextStyle(fontSize: 14),
),
onTap: () {
Get.back();
showModalBottomSheet(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => RepostPanel(
rid: videoDetail.aid,
dynType: 8,
pic: videoDetail.pic,
title: videoDetail.title,
uname: videoDetail.owner?.name,
),
);
},
),
ListTile(
dense: true,
title: const Text(
'分享至消息',
style: TextStyle(fontSize: 14),
),
onTap: () {
Get.back();
try {
PageUtils.pmShare(
context,
content: {
"id": videoDetail.aid!.toString(),
"title": videoDetail.title!,
"headline": videoDetail.title!,
"source": 5,
"thumb": videoDetail.pic!,
"author": videoDetail.owner!.name!,
"author_id": videoDetail.owner!.mid!.toString(),
},
);
} catch (e) {
SmartDialog.showToast(e.toString());
}
},
),
],
),
),
);
}
// 查询关注状态
Future<void> queryFollowStatus() async {
final videoDetail = this.videoDetail.value;
if (videoDetail.owner == null || videoDetail.staff?.isNotEmpty == true) {
return;
}
final res = await UserHttp.hasFollow(videoDetail.owner!.mid!);
if (res case Success(:final response)) {
if (response['special'] == 1) response['attribute'] = -10;
followStatus.value = response;
}
}
// 关注/取关up
Future<void> actionRelationMod(BuildContext context) async {
if (!isLogin) {
SmartDialog.showToast('账号未登录');
return;
}
final videoDetail = this.videoDetail.value;
if (videoDetail.staff?.isNotEmpty == true) {
return;
}
int? mid = videoDetail.owner?.mid;
if (mid == null) {
return;
}
int attr = followStatus['attribute'] ?? 0;
if (attr == 128) {
final res = await VideoHttp.relationMod(
mid: mid,
act: 6,
reSrc: 11,
);
if (res.isSuccess) {
followStatus['attribute'] = 0;
}
return;
} else {
RequestUtils.actionRelationMod(
context: context,
mid: mid,
isFollow: attr != 0,
followStatus: followStatus,
afterMod: (attribute) {
followStatus['attribute'] = attribute;
Future.delayed(const Duration(milliseconds: 500), queryFollowStatus);
},
);
}
}
// 修改分P或番剧分集
Future<bool> onChangeEpisode(
BaseEpisodeItem episode, {
bool isStein = false,
}) async {
try {
final String bvid = episode.bvid ?? this.bvid;
final int aid = episode.aid ?? IdUtils.bv2av(bvid);
final int? cid =
episode.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
if (cid == null) {
return false;
}
final String? cover = episode.cover;
// 重新获取视频资源
if (videoDetailCtr.isPlayAll) {
if (videoDetailCtr.mediaList.indexWhere((item) => item.bvid == bvid) ==
-1) {
PageUtils.toVideoPage(
bvid: bvid,
cid: cid,
cover: cover,
);
return false;
}
}
videoDetailCtr
..plPlayerController.pause()
..makeHeartBeat()
..updateMediaListHistory(aid)
..onReset(isStein: isStein)
..bvid = bvid
..aid = aid
..cid.value = cid
..queryVideoUrl();
if (this.bvid != bvid) {
reload = true;
aiConclusionResult = null;
if (cover != null && cover.isNotEmpty) {
videoDetailCtr.cover.value = cover;
}
// 重新请求相关视频
if (videoDetailCtr.plPlayerController.showRelatedVideo) {
try {
Get.find<RelatedController>(tag: heroTag)
..bvid = bvid
..queryData();
} catch (_) {}
}
// 重新请求评论
if (videoDetailCtr.showReply) {
try {
final replyCtr = Get.find<VideoReplyController>(tag: heroTag)
..aid = aid;
if (replyCtr.loadingState.value is! Loading) {
replyCtr.onReload();
}
} catch (_) {}
}
hasLater.value = videoDetailCtr.sourceType == SourceType.watchLater;
this.bvid = bvid;
queryVideoIntro();
} else {
if (episode is Part) {
final videoDetail = this.videoDetail.value;
videoPlayerServiceHandler?.onVideoDetailChange(
episode,
cid,
heroTag,
artist: videoDetail.owner?.name,
cover: videoDetail.pic,
);
}
}
this.cid.value = cid;
queryOnlineTotal();
return true;
} catch (e) {
if (kDebugMode) debugPrint('ugc onChangeEpisode: $e');
return false;
}
}
@override
void onClose() {
expandableCtr.dispose();
super.onClose();
}
/// 播放上一个
@override
bool prevPlay([bool skipPart = false]) {
final List<BaseEpisodeItem> episodes = <BaseEpisodeItem>[];
bool isPart = false;
final videoDetail = this.videoDetail.value;
if (!skipPart && (videoDetail.pages?.length ?? 0) > 1) {
isPart = true;
episodes.addAll(videoDetail.pages!);
} else if (videoDetailCtr.isPlayAll) {
episodes.addAll(videoDetailCtr.mediaList);
} else if (videoDetail.ugcSeason != null) {
final UgcSeason ugcSeason = videoDetail.ugcSeason!;
final List<SectionItem> sections = ugcSeason.sections!;
for (int i = 0; i < sections.length; i++) {
final List<EpisodeItem> episodesList = sections[i].episodes!;
episodes.addAll(episodesList);
}
}
final int currentIndex = episodes.indexWhere(
(e) =>
e.cid ==
(skipPart
? videoDetail.isPageReversed
? videoDetail.pages!.last.cid
: videoDetail.pages!.first.cid
: this.cid.value),
);
int prevIndex = currentIndex - 1;
final PlayRepeat playRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环
if (prevIndex < 0) {
if (isPart &&
(videoDetailCtr.isPlayAll || videoDetail.ugcSeason != null)) {
return prevPlay(true);
}
if (playRepeat == PlayRepeat.listCycle) {
prevIndex = episodes.length - 1;
} else {
return false;
}
}
int? cid = episodes[prevIndex].cid;
while (cid == null) {
prevIndex--;
if (prevIndex < 0) {
return false;
}
cid = episodes[prevIndex].cid;
}
if (cid != this.cid.value) {
onChangeEpisode(episodes[prevIndex]);
return true;
} else {
return false;
}
}
/// 列表循环或者顺序播放时,自动播放下一个
@override
bool nextPlay([bool skipPart = false]) {
try {
final List<BaseEpisodeItem> episodes = <BaseEpisodeItem>[];
bool isPart = false;
final videoDetail = this.videoDetail.value;
// part -> playall -> season
if (!skipPart && (videoDetail.pages?.length ?? 0) > 1) {
isPart = true;
final List<Part> pages = videoDetail.pages!;
episodes.addAll(pages);
} else if (videoDetailCtr.isPlayAll) {
episodes.addAll(videoDetailCtr.mediaList);
} else if (videoDetail.ugcSeason != null) {
final UgcSeason ugcSeason = videoDetail.ugcSeason!;
final List<SectionItem> sections = ugcSeason.sections!;
for (int i = 0; i < sections.length; i++) {
final List<EpisodeItem> episodesList = sections[i].episodes!;
episodes.addAll(episodesList);
}
}
final PlayRepeat playRepeat =
videoDetailCtr.plPlayerController.playRepeat;
if (episodes.isEmpty) {
if (playRepeat == PlayRepeat.listCycle) {
videoDetailCtr.plPlayerController.play(repeat: true);
return true;
}
if (playRepeat == PlayRepeat.autoPlayRelated &&
videoDetailCtr.plPlayerController.showRelatedVideo) {
return playRelated();
}
return false;
}
final int currentIndex = episodes.indexWhere(
(e) =>
e.cid ==
(skipPart
? videoDetail.isPageReversed
? videoDetail.pages!.last.cid
: videoDetail.pages!.first.cid
: this.cid.value),
);
int nextIndex = currentIndex + 1;
if (!isPart &&
videoDetailCtr.isPlayAll &&
currentIndex == episodes.length - 2) {
videoDetailCtr.getMediaList();
}
// 列表循环
if (nextIndex >= episodes.length) {
if (isPart &&
(videoDetailCtr.isPlayAll || videoDetail.ugcSeason != null)) {
return nextPlay(true);
}
if (playRepeat == PlayRepeat.listCycle) {
nextIndex = 0;
} else if (playRepeat == PlayRepeat.autoPlayRelated &&
videoDetailCtr.plPlayerController.showRelatedVideo) {
return playRelated();
} else {
return false;
}
}
int? cid = episodes[nextIndex].cid;
while (cid == null) {
nextIndex++;
if (nextIndex >= episodes.length) {
return false;
}
cid = episodes[nextIndex].cid;
}
if (cid != this.cid.value) {
onChangeEpisode(episodes[nextIndex]);
return true;
} else {
return false;
}
} catch (_) {
return false;
}
}
bool playRelated() {
RelatedController relatedCtr;
if (Get.isRegistered<RelatedController>(tag: heroTag)) {
relatedCtr = Get.find<RelatedController>(tag: heroTag);
} else {
relatedCtr = Get.put(RelatedController(autoQuery: false), tag: heroTag)
..queryData().whenComplete(playRelated);
return false;
}
if (relatedCtr.loadingState.value case Success(:final response)) {
final firstItem = response?.firstOrNull;
if (firstItem == null) {
SmartDialog.showToast('暂无相关视频,停止连播');
return false;
}
onChangeEpisode(
BaseEpisodeItem(
aid: firstItem.aid,
bvid: firstItem.bvid,
cid: firstItem.cid,
cover: firstItem.cover,
),
);
return true;
}
return false;
}
// ai总结
static Future<AiConclusionResult?> getAiConclusion(
String bvid,
int cid,
int? mid,
) async {
if (!Accounts.heartbeat.isLogin) {
SmartDialog.showToast("账号未登录");
return null;
}
SmartDialog.showLoading(msg: '正在获取AI总结');
final res = await VideoHttp.aiConclusion(
bvid: bvid,
cid: cid,
upMid: mid,
);
SmartDialog.dismiss();
if (res case Success(:final response)) {
return response.modelResult;
} else if (res is Error && res.code == 1) {
SmartDialog.showToast("AI处理中请稍后再试");
} else {
SmartDialog.showToast("当前视频暂不支持AI视频总结");
}
return null;
}
Future<void> aiConclusion() async {
aiConclusionResult = await getAiConclusion(
bvid,
cid.value,
videoDetail.value.owner?.mid,
);
}
}