merge main
This commit is contained in:
@ -1,4 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -14,13 +16,16 @@ import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:pilipala/utils/video_utils.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
|
||||
import 'widgets/header_control.dart';
|
||||
|
||||
class VideoDetailController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
/// 路由传参
|
||||
String bvid = Get.parameters['bvid']!;
|
||||
int cid = int.parse(Get.parameters['cid']!);
|
||||
RxInt cid = int.parse(Get.parameters['cid']!).obs;
|
||||
RxInt danmakuCid = 0.obs;
|
||||
String heroTag = Get.arguments['heroTag'];
|
||||
// 视频详情
|
||||
@ -76,6 +81,13 @@ class VideoDetailController extends GetxController
|
||||
bool enableHeart = true;
|
||||
var userInfo;
|
||||
late bool isFirstTime = true;
|
||||
Floating? floating;
|
||||
late PreferredSizeWidget headerControl;
|
||||
|
||||
late bool enableCDN;
|
||||
late int? cacheVideoQa;
|
||||
late String cacheDecode;
|
||||
late int cacheAudioQa;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -103,7 +115,26 @@ class VideoDetailController extends GetxController
|
||||
localCache.get(LocalCacheKey.historyPause) == true) {
|
||||
enableHeart = false;
|
||||
}
|
||||
danmakuCid.value = cid;
|
||||
danmakuCid.value = cid.value;
|
||||
|
||||
///
|
||||
if (Platform.isAndroid) {
|
||||
floating = Floating();
|
||||
}
|
||||
headerControl = HeaderControl(
|
||||
controller: plPlayerController,
|
||||
videoDetailCtr: this,
|
||||
floating: floating,
|
||||
);
|
||||
// CDN优化
|
||||
enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);
|
||||
// 预设的画质
|
||||
cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa);
|
||||
// 预设的解码格式
|
||||
cacheDecode = setting.get(SettingBoxKey.defaultDecode,
|
||||
defaultValue: VideoDecodeFormats.values.last.code);
|
||||
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||
defaultValue: AudioQuality.hiRes.code);
|
||||
}
|
||||
|
||||
showReplyReplyPanel() {
|
||||
@ -167,7 +198,13 @@ class VideoDetailController extends GetxController
|
||||
playerInit();
|
||||
}
|
||||
|
||||
Future playerInit({video, audio, seekToTime, duration}) async {
|
||||
Future playerInit({
|
||||
video,
|
||||
audio,
|
||||
seekToTime,
|
||||
duration,
|
||||
bool autoplay = true,
|
||||
}) async {
|
||||
/// 设置/恢复 屏幕亮度
|
||||
if (brightness != null) {
|
||||
ScreenBrightness().setScreenBrightness(brightness!);
|
||||
@ -193,36 +230,35 @@ class VideoDetailController extends GetxController
|
||||
direction: (firstVideo.width! - firstVideo.height!) > 0
|
||||
? 'horizontal'
|
||||
: 'vertical',
|
||||
// 默认1倍速
|
||||
speed: 1.0,
|
||||
bvid: bvid,
|
||||
cid: cid,
|
||||
cid: cid.value,
|
||||
enableHeart: enableHeart,
|
||||
isFirstTime: isFirstTime,
|
||||
autoplay: autoplay,
|
||||
);
|
||||
|
||||
/// 开启自动全屏时,在player初始化完成后立即传入headerControl
|
||||
plPlayerController.headerControl = headerControl;
|
||||
}
|
||||
|
||||
// 视频链接
|
||||
Future queryVideoUrl() async {
|
||||
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid);
|
||||
var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
|
||||
if (result['status']) {
|
||||
data = result['data'];
|
||||
|
||||
List<VideoItem> allVideosList = data.dash!.video!;
|
||||
|
||||
try {
|
||||
// 当前可播放的最高质量视频
|
||||
int currentHighVideoQa = allVideosList.first.quality!.code;
|
||||
// 使用预设的画质 | 当前可用的最高质量
|
||||
int cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
|
||||
defaultValue: currentHighVideoQa);
|
||||
// 预设的画质为null,则当前可用的最高质量
|
||||
cacheVideoQa ??= currentHighVideoQa;
|
||||
int resVideoQa = currentHighVideoQa;
|
||||
if (cacheVideoQa <= currentHighVideoQa) {
|
||||
if (cacheVideoQa! <= currentHighVideoQa) {
|
||||
// 如果预设的画质低于当前最高
|
||||
List<int> numbers = data.acceptQuality!
|
||||
.where((e) => e <= currentHighVideoQa)
|
||||
.toList();
|
||||
resVideoQa = Utils.findClosestNumber(cacheVideoQa, numbers);
|
||||
resVideoQa = Utils.findClosestNumber(cacheVideoQa!, numbers);
|
||||
}
|
||||
currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!;
|
||||
|
||||
@ -236,9 +272,7 @@ class VideoDetailController extends GetxController
|
||||
List supportDecodeFormats =
|
||||
supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!;
|
||||
// 默认从设置中取AVC
|
||||
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
|
||||
SettingBoxKey.defaultDecode,
|
||||
defaultValue: VideoDecodeFormats.values.last.code))!;
|
||||
currentDecodeFormats = VideoDecodeFormatsCode.fromString(cacheDecode)!;
|
||||
try {
|
||||
// 当前视频没有对应格式返回第一个
|
||||
bool flag = false;
|
||||
@ -250,8 +284,8 @@ class VideoDetailController extends GetxController
|
||||
currentDecodeFormats = flag
|
||||
? currentDecodeFormats
|
||||
: VideoDecodeFormatsCode.fromString(supportDecodeFormats.first)!;
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast('DecodeFormats error: $err');
|
||||
}
|
||||
|
||||
/// 取出符合当前解码格式的videoItem
|
||||
@ -261,9 +295,11 @@ class VideoDetailController extends GetxController
|
||||
} catch (_) {
|
||||
firstVideo = videosList.first;
|
||||
}
|
||||
videoUrl = firstVideo.baseUrl!;
|
||||
videoUrl = enableCDN
|
||||
? VideoUtils.getCdnUrl(firstVideo)
|
||||
: (firstVideo.backupUrl ?? firstVideo.baseUrl!);
|
||||
} catch (err) {
|
||||
print(err);
|
||||
SmartDialog.showToast('firstVideo error: $err');
|
||||
}
|
||||
|
||||
/// 优先顺序 设置中指定质量 -> 当前可选的最高质量
|
||||
@ -271,9 +307,6 @@ class VideoDetailController extends GetxController
|
||||
List<AudioItem> audiosList = data.dash!.audio!;
|
||||
|
||||
try {
|
||||
int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||
defaultValue: AudioQuality.hiRes.code);
|
||||
|
||||
if (data.dash!.dolby?.audio?.isNotEmpty == true) {
|
||||
// 杜比
|
||||
audiosList.insert(0, data.dash!.dolby!.audio!.first);
|
||||
@ -286,14 +319,23 @@ class VideoDetailController extends GetxController
|
||||
|
||||
if (audiosList.isNotEmpty) {
|
||||
List<int> numbers = audiosList.map((map) => map.id!).toList();
|
||||
int closestNumber = Utils.findClosestNumber(resultAudioQa, numbers);
|
||||
int closestNumber = Utils.findClosestNumber(cacheAudioQa, numbers);
|
||||
if (!numbers.contains(cacheAudioQa) &&
|
||||
numbers.any((e) => e > cacheAudioQa)) {
|
||||
closestNumber = 30280;
|
||||
}
|
||||
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);
|
||||
} else {
|
||||
firstAudio = AudioItem();
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} catch (err) {
|
||||
firstAudio = audiosList.isNotEmpty ? audiosList.first : AudioItem();
|
||||
SmartDialog.showToast('firstAudio error: $err');
|
||||
}
|
||||
|
||||
audioUrl = firstAudio!.baseUrl ?? '';
|
||||
audioUrl = enableCDN
|
||||
? VideoUtils.getCdnUrl(firstAudio)
|
||||
: (firstAudio.backupUrl ?? firstAudio.baseUrl!);
|
||||
//
|
||||
if (firstAudio.id != null) {
|
||||
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;
|
||||
|
||||
@ -8,14 +8,18 @@ import 'package:pilipala/http/constants.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/user/fav_folder.dart';
|
||||
import 'package:pilipala/models/video/ai.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import 'widgets/group_panel.dart';
|
||||
|
||||
class VideoIntroController extends GetxController {
|
||||
// 视频bvid
|
||||
String bvid = Get.parameters['bvid']!;
|
||||
@ -58,11 +62,16 @@ class VideoIntroController extends GetxController {
|
||||
RxString total = '1'.obs;
|
||||
Timer? timer;
|
||||
bool isPaused = false;
|
||||
String heroTag = '';
|
||||
late ModelResult modelResult;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
try {
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
} catch (_) {}
|
||||
if (Get.arguments.isNotEmpty) {
|
||||
if (Get.arguments.containsKey('videoItem')) {
|
||||
preRender = true;
|
||||
@ -102,9 +111,10 @@ class VideoIntroController extends GetxController {
|
||||
if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {
|
||||
lastPlayCid.value = videoDetail.value.pages!.first.cid!;
|
||||
}
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
|
||||
.tabs
|
||||
.value = ['简介', '评论 ${result['data']!.stat!.reply}'];
|
||||
// Get.find<VideoDetailController>(tag: heroTag).tabs.value = [
|
||||
// '简介',
|
||||
// '评论 ${result['data']!.stat!.reply}'
|
||||
// ];
|
||||
// 获取到粉丝数再返回
|
||||
await queryUserStat();
|
||||
}
|
||||
@ -330,7 +340,8 @@ class VideoIntroController extends GetxController {
|
||||
|
||||
// 分享视频
|
||||
Future actionShareVideo() async {
|
||||
var result = await Share.share('${HttpString.baseUrl}/video/$bvid')
|
||||
var result = await Share.share(
|
||||
'${videoDetail.value.title} - ${HttpString.baseUrl}/video/$bvid')
|
||||
.whenComplete(() {});
|
||||
return result;
|
||||
}
|
||||
@ -424,6 +435,20 @@ class VideoIntroController extends GetxController {
|
||||
}
|
||||
followStatus['attribute'] = actionStatus;
|
||||
followStatus.refresh();
|
||||
if (actionStatus == 2) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('关注成功'),
|
||||
duration: const Duration(seconds: 2),
|
||||
action: SnackBarAction(
|
||||
label: '设置分组',
|
||||
onPressed: setFollowGroup,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
SmartDialog.dismiss();
|
||||
},
|
||||
@ -439,16 +464,16 @@ class VideoIntroController extends GetxController {
|
||||
Future changeSeasonOrbangu(bvid, cid, aid) async {
|
||||
// 重新获取视频资源
|
||||
VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
||||
Get.find<VideoDetailController>(tag: heroTag);
|
||||
videoDetailCtr.bvid = bvid;
|
||||
videoDetailCtr.cid = cid;
|
||||
videoDetailCtr.cid.value = cid;
|
||||
videoDetailCtr.danmakuCid.value = cid;
|
||||
videoDetailCtr.queryVideoUrl();
|
||||
// 重新请求评论
|
||||
try {
|
||||
/// 未渲染回复组件时可能异常
|
||||
VideoReplyController videoReplyCtr =
|
||||
Get.find<VideoReplyController>(tag: Get.arguments['heroTag']);
|
||||
Get.find<VideoReplyController>(tag: heroTag);
|
||||
videoReplyCtr.aid = aid;
|
||||
videoReplyCtr.queryReplyList(type: 'init');
|
||||
} catch (_) {}
|
||||
@ -485,4 +510,74 @@ class VideoIntroController extends GetxController {
|
||||
}
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// 列表循环或者顺序播放时,自动播放下一个
|
||||
void nextPlay() {
|
||||
late List episodes;
|
||||
bool isPages = false;
|
||||
if (videoDetail.value.ugcSeason != null) {
|
||||
UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
|
||||
List<SectionItem> sections = ugcSeason.sections!;
|
||||
episodes = [];
|
||||
|
||||
for (int i = 0; i < sections.length; i++) {
|
||||
List<EpisodeItem> episodesList = sections[i].episodes!;
|
||||
episodes.addAll(episodesList);
|
||||
}
|
||||
} else if (videoDetail.value.pages != null) {
|
||||
isPages = true;
|
||||
List<Part> pages = videoDetail.value.pages!;
|
||||
episodes = [];
|
||||
episodes.addAll(pages);
|
||||
}
|
||||
|
||||
int currentIndex = episodes.indexWhere((e) => e.cid == lastPlayCid.value);
|
||||
int nextIndex = currentIndex + 1;
|
||||
VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: heroTag);
|
||||
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
|
||||
|
||||
// 列表循环
|
||||
if (nextIndex >= episodes.length) {
|
||||
if (platRepeat == PlayRepeat.listCycle) {
|
||||
nextIndex = 0;
|
||||
}
|
||||
if (platRepeat == PlayRepeat.listOrder) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
int cid = episodes[nextIndex].cid!;
|
||||
String rBvid = isPages ? bvid : episodes[nextIndex].bvid;
|
||||
int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;
|
||||
changeSeasonOrbangu(rBvid, cid, rAid);
|
||||
}
|
||||
|
||||
// 设置关注分组
|
||||
void setFollowGroup() {
|
||||
Get.bottomSheet(
|
||||
GroupPanel(mid: videoDetail.value.owner!.mid!),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
// ai总结
|
||||
Future aiConclusion() async {
|
||||
SmartDialog.showLoading(msg: '正在生产ai总结');
|
||||
var res = await VideoHttp.aiConclusion(
|
||||
bvid: bvid,
|
||||
cid: lastPlayCid.value,
|
||||
upMid: videoDetail.value.owner!.mid!,
|
||||
);
|
||||
if (res['status']) {
|
||||
if (res['data'].modelResult.resultType == 0) {
|
||||
SmartDialog.showToast('该视频不支持ai总结');
|
||||
}
|
||||
if (res['data'].modelResult.resultType == 2 ||
|
||||
res['data'].modelResult.resultType == 1) {
|
||||
modelResult = res['data'].modelResult;
|
||||
}
|
||||
}
|
||||
SmartDialog.dismiss();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,8 @@ import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||
import 'package:pilipala/common/widgets/stat/view.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
@ -199,6 +201,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
|
||||
// 视频介绍
|
||||
showIntroDetail() {
|
||||
if (loadingStatus) {
|
||||
return;
|
||||
}
|
||||
feedBack();
|
||||
showBottomSheet(
|
||||
context: context,
|
||||
@ -223,6 +228,17 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
arguments: {'face': face, 'heroTag': memberHeroTag});
|
||||
}
|
||||
|
||||
// ai总结
|
||||
showAiBottomSheet() {
|
||||
showBottomSheet(
|
||||
context: context,
|
||||
enableDrag: true,
|
||||
builder: (BuildContext context) {
|
||||
return AiDetail(modelResult: videoIntroController.modelResult);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ThemeData t = Theme.of(context);
|
||||
@ -238,103 +254,100 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => showIntroDetail(),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
!loadingStatus
|
||||
? widget.videoDetail!.title
|
||||
: videoItem['title'],
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding:
|
||||
MaterialStateProperty.all(EdgeInsets.zero),
|
||||
backgroundColor:
|
||||
MaterialStateProperty.resolveWith((states) {
|
||||
return t.highlightColor.withOpacity(0.2);
|
||||
}),
|
||||
),
|
||||
onPressed: showIntroDetail,
|
||||
icon: Icon(
|
||||
Icons.more_horiz,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
!loadingStatus
|
||||
? widget.videoDetail!.title
|
||||
: videoItem['title'],
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => showIntroDetail(),
|
||||
child: Row(
|
||||
children: [
|
||||
StatView(
|
||||
theme: 'gray',
|
||||
view: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.view
|
||||
: videoItem['stat'].view,
|
||||
size: 'medium',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
StatDanMu(
|
||||
theme: 'gray',
|
||||
danmu: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.danmaku
|
||||
: videoItem['stat'].danmaku,
|
||||
size: 'medium',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
Utils.dateFormat(
|
||||
!widget.loadingStatus
|
||||
? widget.videoDetail!.pubdate
|
||||
: videoItem['pubdate'],
|
||||
formatType: 'detail'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (videoIntroController.isShowOnlineTotal)
|
||||
Obx(
|
||||
() => Text(
|
||||
'${videoIntroController.total.value}人在看',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => showIntroDetail(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 7, bottom: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
StatView(
|
||||
theme: 'gray',
|
||||
view: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.view
|
||||
: videoItem['stat'].view,
|
||||
size: 'medium',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
StatDanMu(
|
||||
theme: 'gray',
|
||||
danmu: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.danmaku
|
||||
: videoItem['stat'].danmaku,
|
||||
size: 'medium',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
Utils.dateFormat(
|
||||
!widget.loadingStatus
|
||||
? widget.videoDetail!.pubdate
|
||||
: videoItem['pubdate'],
|
||||
formatType: 'detail'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (videoIntroController.isShowOnlineTotal)
|
||||
Obx(
|
||||
() => Text(
|
||||
'${videoIntroController.total.value}人在看',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 10,
|
||||
top: 6,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
var res = await videoIntroController.aiConclusion();
|
||||
if (res['status']) {
|
||||
if (res['data'].modelResult.resultType == 2 ||
|
||||
res['data'].modelResult.resultType == 1) {
|
||||
showAiBottomSheet();
|
||||
}
|
||||
}
|
||||
},
|
||||
child:
|
||||
Image.asset('assets/images/ai.png', height: 22),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
// 点赞收藏转发 布局样式1
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(top: 7, bottom: 7),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: actionRow(
|
||||
context,
|
||||
videoIntroController,
|
||||
videoDetailCtr,
|
||||
),
|
||||
),
|
||||
// SingleChildScrollView(
|
||||
// padding: const EdgeInsets.only(top: 7, bottom: 7),
|
||||
// scrollDirection: Axis.horizontal,
|
||||
// child: actionRow(
|
||||
// context,
|
||||
// videoIntroController,
|
||||
// videoDetailCtr,
|
||||
// ),
|
||||
// ),
|
||||
// 点赞收藏转发 布局样式2
|
||||
// actionGrid(context, videoIntroController),
|
||||
actionGrid(context, videoIntroController),
|
||||
// 合集
|
||||
if (!loadingStatus &&
|
||||
widget.videoDetail!.ugcSeason != null) ...[
|
||||
@ -452,7 +465,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
Widget actionGrid(BuildContext context, videoIntroController) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 6, bottom: 10),
|
||||
margin: const EdgeInsets.only(top: 6, bottom: 4),
|
||||
height: constraints.maxWidth / 5 * 0.8,
|
||||
child: GridView.count(
|
||||
primary: false,
|
||||
@ -471,12 +484,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
? widget.videoDetail!.stat!.like!.toString()
|
||||
: '-'),
|
||||
),
|
||||
ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.clock),
|
||||
onTap: () => videoIntroController.actionShareVideo(),
|
||||
selectStatus: false,
|
||||
loadingStatus: loadingStatus,
|
||||
text: '稍后再看'),
|
||||
// ActionItem(
|
||||
// icon: const Icon(FontAwesomeIcons.clock),
|
||||
// onTap: () => videoIntroController.actionShareVideo(),
|
||||
// selectStatus: false,
|
||||
// loadingStatus: loadingStatus,
|
||||
// text: '稍后再看'),
|
||||
Obx(
|
||||
() => ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.b),
|
||||
@ -492,22 +505,28 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
() => ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.star),
|
||||
selectIcon: const Icon(FontAwesomeIcons.solidStar),
|
||||
// onTap: () => videoIntroController.actionFavVideo(),
|
||||
onTap: () => showFavBottomSheet(),
|
||||
onLongPress: () => showFavBottomSheet(type: 'longPress'),
|
||||
selectStatus: videoIntroController.hasFav.value,
|
||||
loadingStatus: loadingStatus,
|
||||
text: !loadingStatus
|
||||
? widget.videoDetail!.stat!.favorite!.toString()
|
||||
: '-'),
|
||||
),
|
||||
ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.comment),
|
||||
onTap: () => videoDetailCtr.tabCtr.animateTo(1),
|
||||
selectStatus: false,
|
||||
loadingStatus: loadingStatus,
|
||||
text: !loadingStatus
|
||||
? widget.videoDetail!.stat!.reply!.toString()
|
||||
: '评论'),
|
||||
ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.shareFromSquare),
|
||||
onTap: () => videoIntroController.actionShareVideo(),
|
||||
selectStatus: false,
|
||||
loadingStatus: loadingStatus,
|
||||
text: !loadingStatus
|
||||
? widget.videoDetail!.stat!.share!.toString()
|
||||
: '-'),
|
||||
text: '分享'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -6,6 +6,7 @@ class ActionItem extends StatelessWidget {
|
||||
final Icon? icon;
|
||||
final Icon? selectIcon;
|
||||
final Function? onTap;
|
||||
final Function? onLongPress;
|
||||
final bool? loadingStatus;
|
||||
final String? text;
|
||||
final bool selectStatus;
|
||||
@ -15,6 +16,7 @@ class ActionItem extends StatelessWidget {
|
||||
this.icon,
|
||||
this.selectIcon,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.loadingStatus,
|
||||
this.text,
|
||||
this.selectStatus = false,
|
||||
@ -27,6 +29,9 @@ class ActionItem extends StatelessWidget {
|
||||
feedBack(),
|
||||
onTap!(),
|
||||
},
|
||||
onLongPress: () => {
|
||||
if (onLongPress != null) {onLongPress!()}
|
||||
},
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
156
lib/pages/video/detail/introduction/widgets/group_panel.dart
Normal file
156
lib/pages/video/detail/introduction/widgets/group_panel.dart
Normal file
@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/http/member.dart';
|
||||
import 'package:pilipala/models/member/tags.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class GroupPanel extends StatefulWidget {
|
||||
final int? mid;
|
||||
const GroupPanel({super.key, this.mid});
|
||||
|
||||
@override
|
||||
State<GroupPanel> createState() => _GroupPanelState();
|
||||
}
|
||||
|
||||
class _GroupPanelState extends State<GroupPanel> {
|
||||
Box localCache = GStrorage.localCache;
|
||||
late double sheetHeight;
|
||||
late Future _futureBuilderFuture;
|
||||
late List<MemberTagItemModel> tagsList;
|
||||
bool showDefault = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
sheetHeight = localCache.get('sheetHeight');
|
||||
_futureBuilderFuture = MemberHttp.followUpTags();
|
||||
}
|
||||
|
||||
void onSave() async {
|
||||
feedBack();
|
||||
// 是否有选中的 有选中的带id,没选使用默认0
|
||||
bool anyHasChecked = tagsList.any((e) => e.checked == true);
|
||||
late String tagids;
|
||||
if (anyHasChecked) {
|
||||
List checkedList = tagsList.where((e) => e.checked == true).toList();
|
||||
List<int> tagidList = checkedList.map<int>((e) => e.tagid).toList();
|
||||
tagids = tagidList.join(',');
|
||||
} else {
|
||||
tagids = '0';
|
||||
}
|
||||
// 保存
|
||||
var res = await MemberHttp.addUsers(widget.mid, tagids);
|
||||
SmartDialog.showToast(res['msg']);
|
||||
if (res['status']) {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: sheetHeight,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
AppBar(
|
||||
centerTitle: false,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.close_outlined)),
|
||||
title:
|
||||
Text('设置关注分组', style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
tagsList = data['data'];
|
||||
return ListView.builder(
|
||||
itemCount: data['data'].length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
data['data'][index].checked =
|
||||
!data['data'][index].checked;
|
||||
showDefault =
|
||||
!data['data'].any((e) => e.checked == true);
|
||||
setState(() {});
|
||||
},
|
||||
dense: true,
|
||||
leading: const Icon(Icons.group_outlined),
|
||||
minLeadingWidth: 0,
|
||||
title: Text(data['data'][index].name),
|
||||
subtitle: data['data'][index].tip != ''
|
||||
? Text(data['data'][index].tip)
|
||||
: null,
|
||||
trailing: Transform.scale(
|
||||
scale: 0.9,
|
||||
child: Checkbox(
|
||||
value: data['data'][index].checked,
|
||||
onChanged: (bool? checkValue) {
|
||||
data['data'][index].checked = checkValue;
|
||||
showDefault = !data['data']
|
||||
.any((e) => e.checked == true);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const Text('请求中');
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).disabledColor.withOpacity(0.08),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 12,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => onSave(),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.only(left: 30, right: 30),
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary, // 设置按钮背景色
|
||||
),
|
||||
child: Text(showDefault ? '保存至默认分组' : '保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||
@ -129,7 +130,50 @@ class IntroDetail extends StatelessWidget {
|
||||
final currentDesc = descV2[index];
|
||||
switch (currentDesc.type) {
|
||||
case 1:
|
||||
return TextSpan(text: currentDesc.rawText);
|
||||
List<InlineSpan> spanChildren = [];
|
||||
RegExp urlRegExp = RegExp(r'https?://\S+\b');
|
||||
Iterable<Match> matches = urlRegExp.allMatches(currentDesc.rawText);
|
||||
|
||||
int previousEndIndex = 0;
|
||||
for (Match match in matches) {
|
||||
if (match.start > previousEndIndex) {
|
||||
spanChildren.add(TextSpan(
|
||||
text: currentDesc.rawText
|
||||
.substring(previousEndIndex, match.start)));
|
||||
}
|
||||
spanChildren.add(
|
||||
TextSpan(
|
||||
text: match.group(0),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 处理点击事件
|
||||
try {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': match.group(0)!,
|
||||
'type': 'url',
|
||||
'pageTitle': match.group(0)!,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
previousEndIndex = match.end;
|
||||
}
|
||||
|
||||
if (previousEndIndex < currentDesc.rawText.length) {
|
||||
spanChildren.add(TextSpan(
|
||||
text: currentDesc.rawText.substring(previousEndIndex)));
|
||||
}
|
||||
|
||||
TextSpan result = TextSpan(children: spanChildren);
|
||||
return result;
|
||||
case 2:
|
||||
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
|
||||
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
|
||||
|
||||
@ -17,35 +17,31 @@ class MenuRow extends StatelessWidget {
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: [
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'推荐',
|
||||
selectStatus: true,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'弹幕',
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '推荐',
|
||||
selectStatus: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'评论列表',
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '弹幕',
|
||||
selectStatus: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'播放列表',
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '评论列表',
|
||||
selectStatus: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '播放列表',
|
||||
selectStatus: false,
|
||||
),
|
||||
]),
|
||||
@ -99,3 +95,62 @@ class MenuRow extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActionRowLineItem extends StatelessWidget {
|
||||
final bool? selectStatus;
|
||||
final Function? onTap;
|
||||
final bool? loadingStatus;
|
||||
final String? text;
|
||||
|
||||
const ActionRowLineItem(
|
||||
{super.key,
|
||||
this.selectStatus,
|
||||
this.onTap,
|
||||
this.text,
|
||||
this.loadingStatus = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: selectStatus!
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Colors.transparent,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(30)),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: InkWell(
|
||||
onTap: () => {
|
||||
feedBack(),
|
||||
onTap!(),
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(13, 5.5, 13, 4.5),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(30)),
|
||||
border: Border.all(
|
||||
color: selectStatus!
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
opacity: loadingStatus! ? 0 : 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
text!,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: selectStatus!
|
||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||
: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
|
||||
class PagesPanel extends StatefulWidget {
|
||||
final List<Part> pages;
|
||||
@ -22,13 +23,23 @@ class PagesPanel extends StatefulWidget {
|
||||
|
||||
class _PagesPanelState extends State<PagesPanel> {
|
||||
late List<Part> episodes;
|
||||
late int cid;
|
||||
late int currentIndex;
|
||||
String heroTag = Get.arguments['heroTag'];
|
||||
late VideoDetailController _videoDetailController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
cid = widget.cid!;
|
||||
episodes = widget.pages;
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
|
||||
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == cid);
|
||||
_videoDetailController.cid.listen((p0) {
|
||||
cid = p0;
|
||||
setState(() {});
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == cid);
|
||||
});
|
||||
}
|
||||
|
||||
void changeFucCall(item, i) async {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
|
||||
class SeasonPanel extends StatefulWidget {
|
||||
@ -23,11 +24,16 @@ class SeasonPanel extends StatefulWidget {
|
||||
|
||||
class _SeasonPanelState extends State<SeasonPanel> {
|
||||
late List<EpisodeItem> episodes;
|
||||
late int cid;
|
||||
late int currentIndex;
|
||||
String heroTag = Get.arguments['heroTag'];
|
||||
late VideoDetailController _videoDetailController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
cid = widget.cid!;
|
||||
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
|
||||
|
||||
/// 根据 cid 找到对应集,找到对应 episodes
|
||||
/// 有多个episodes时,只显示其中一个
|
||||
@ -36,7 +42,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
for (int i = 0; i < sections.length; i++) {
|
||||
List<EpisodeItem> episodesList = sections[i].episodes!;
|
||||
for (int j = 0; j < episodesList.length; j++) {
|
||||
if (episodesList[j].cid == widget.cid) {
|
||||
if (episodesList[j].cid == cid) {
|
||||
episodes = episodesList;
|
||||
continue;
|
||||
}
|
||||
@ -47,7 +53,12 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
// episodes = widget.ugcSeason.sections!
|
||||
// .firstWhere((e) => e.seasonId == widget.ugcSeason.id)
|
||||
// .episodes!;
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == cid);
|
||||
_videoDetailController.cid.listen((p0) {
|
||||
cid = p0;
|
||||
setState(() {});
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == cid);
|
||||
});
|
||||
}
|
||||
|
||||
void changeFucCall(item, i) async {
|
||||
@ -57,7 +68,9 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
item.aid,
|
||||
);
|
||||
currentIndex = i;
|
||||
setState(() {});
|
||||
Get.back();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/animated_dialog.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/overlay_pop.dart';
|
||||
import 'package:pilipala/common/widgets/video_card_h.dart';
|
||||
import './controller.dart';
|
||||
@ -22,6 +23,9 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
|
||||
future: _releatedController.queryRelatedVideo(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
if (snapshot.data!['status']) {
|
||||
// 请求成功
|
||||
return SliverList(
|
||||
@ -51,9 +55,7 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
|
||||
}, childCount: snapshot.data['data'].length + 1));
|
||||
} else {
|
||||
// 请求错误
|
||||
return const Center(
|
||||
child: Text('出错了'),
|
||||
);
|
||||
return HttpError(errMsg: '出错了', fn: () {});
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
|
||||
@ -92,12 +92,12 @@ class VideoReplyController extends GetxController {
|
||||
}
|
||||
}
|
||||
replies.insertAll(0, res['data'].topReplies);
|
||||
count.value = res['data'].page.count;
|
||||
replyList.value = replies;
|
||||
} else {
|
||||
replyList.addAll(replies);
|
||||
}
|
||||
}
|
||||
count.value = res['data'].page.count;
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -39,6 +39,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
Future? _futureBuilderFuture;
|
||||
bool _isFabVisible = true;
|
||||
String replyLevel = '1';
|
||||
late String heroTag;
|
||||
|
||||
// 添加页面缓存
|
||||
@override
|
||||
@ -46,22 +47,29 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
|
||||
super.initState();
|
||||
int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
replyLevel = widget.replyLevel ?? '1';
|
||||
if (replyLevel == '2') {
|
||||
_videoReplyController = Get.put(
|
||||
VideoReplyController(oid, widget.rpid.toString(), replyLevel),
|
||||
tag: widget.rpid.toString());
|
||||
} else {
|
||||
_videoReplyController = Get.put(VideoReplyController(oid, '', replyLevel),
|
||||
tag: Get.arguments['heroTag']);
|
||||
_videoReplyController =
|
||||
Get.put(VideoReplyController(oid, '', replyLevel), tag: heroTag);
|
||||
}
|
||||
|
||||
fabAnimationCtr = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 300));
|
||||
|
||||
_futureBuilderFuture = _videoReplyController.queryReplyList();
|
||||
|
||||
fabAnimationCtr.forward();
|
||||
scrollListener();
|
||||
}
|
||||
|
||||
void scrollListener() {
|
||||
scrollController = _videoReplyController.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
@ -81,7 +89,6 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
}
|
||||
},
|
||||
);
|
||||
fabAnimationCtr.forward();
|
||||
}
|
||||
|
||||
void _showFab() {
|
||||
@ -101,7 +108,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
// 展示二级回复
|
||||
void replyReply(replyItem) {
|
||||
VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
||||
Get.find<VideoDetailController>(tag: heroTag);
|
||||
if (replyItem != null) {
|
||||
videoDetailCtr.oid = replyItem.oid;
|
||||
videoDetailCtr.fRpid = replyItem.rpid!;
|
||||
@ -112,9 +119,10 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
scrollController.removeListener(() {});
|
||||
fabAnimationCtr.dispose();
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -128,7 +136,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
controller: _videoReplyController.scrollController,
|
||||
controller: scrollController,
|
||||
key: const PageStorageKey<String>('评论'),
|
||||
slivers: <Widget>[
|
||||
SliverPersistentHeader(
|
||||
@ -187,7 +195,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
var data = snapshot.data;
|
||||
if (data['status']) {
|
||||
// 请求成功
|
||||
return Obx(
|
||||
|
||||
@ -7,9 +7,11 @@ import 'package:pilipala/common/widgets/badge.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/common/reply_type.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/pages/preview/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
@ -666,46 +668,71 @@ InlineSpan buildContent(
|
||||
// 匹配 jumpUrl
|
||||
String matchUrl = matchMember;
|
||||
if (content.jumpUrl.isNotEmpty && hasMatchMember) {
|
||||
List urlKeys = content.jumpUrl.keys.toList();
|
||||
List urlKeys = content.jumpUrl.keys.toList().reversed.toList();
|
||||
for (var index = 0; index < urlKeys.length; index++) {
|
||||
var i = urlKeys[index];
|
||||
if (i.contains('?')) {
|
||||
urlKeys[index] = i.replaceAll('?', '\\?');
|
||||
}
|
||||
}
|
||||
matchUrl = matchMember.splitMapJoin(
|
||||
/// RegExp.escape() 转义特殊字符
|
||||
RegExp(RegExp.escape(urlKeys.join("|"))),
|
||||
RegExp(urlKeys.map((key) => key).join("|")),
|
||||
// RegExp('What does the fox say\\?'),
|
||||
onMatch: (Match match) {
|
||||
String matchStr = match[0]!;
|
||||
String appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
|
||||
String appUrlSchema = '';
|
||||
if (content.jumpUrl[matchStr] != null) {
|
||||
appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
|
||||
}
|
||||
// 默认不显示关键词
|
||||
bool enableWordRe =
|
||||
setting.get(SettingBoxKey.enableWordRe, defaultValue: false);
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: content.jumpUrl[matchStr]['title'],
|
||||
style: TextStyle(
|
||||
color: enableWordRe
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
if (appUrlSchema == '') {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': matchStr,
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (appUrlSchema.startsWith('bilibili://search') &&
|
||||
enableWordRe) {
|
||||
Get.toNamed('/searchResult', parameters: {
|
||||
'keyword': content.jumpUrl[matchStr]['title']
|
||||
});
|
||||
if (content.jumpUrl[matchStr] != null) {
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: content.jumpUrl[matchStr]['title'],
|
||||
style: TextStyle(
|
||||
color: enableWordRe
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
if (appUrlSchema == '') {
|
||||
String str = Uri.parse(matchStr).pathSegments[0];
|
||||
Map matchRes = IdUtils.matchAvorBv(input: str);
|
||||
List matchKeys = matchRes.keys.toList();
|
||||
if (matchKeys.isNotEmpty) {
|
||||
if (matchKeys.first == 'BV') {
|
||||
Get.toNamed(
|
||||
'/searchResult',
|
||||
parameters: {'keyword': matchRes['BV']},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': matchStr,
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (appUrlSchema.startsWith('bilibili://search') &&
|
||||
enableWordRe) {
|
||||
Get.toNamed('/searchResult', parameters: {
|
||||
'keyword': content.jumpUrl[matchStr]['title']
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (appUrlSchema.startsWith('bilibili://search') && enableWordRe) {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
@ -743,11 +770,14 @@ InlineSpan buildContent(
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 跳转到指定位置
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
|
||||
.plPlayerController
|
||||
.seekTo(
|
||||
Duration(seconds: Utils.duration(matchStr)),
|
||||
);
|
||||
try {
|
||||
Get.find<VideoDetailController>(
|
||||
tag: Get.arguments['heroTag'])
|
||||
.plPlayerController
|
||||
.seekTo(
|
||||
Duration(seconds: Utils.duration(matchStr)),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -773,7 +803,7 @@ InlineSpan buildContent(
|
||||
|
||||
// 图片渲染
|
||||
if (content.pictures.isNotEmpty) {
|
||||
List picList = [];
|
||||
List<String> picList = [];
|
||||
int len = content.pictures.length;
|
||||
if (len == 1) {
|
||||
Map pictureItem = content.pictures.first;
|
||||
@ -785,8 +815,13 @@ InlineSpan buildContent(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed('/preview',
|
||||
arguments: {'initialPage': 0, 'imgList': picList});
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(initialPage: 0, imgList: picList);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
@ -814,8 +849,13 @@ InlineSpan buildContent(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed('/preview',
|
||||
arguments: {'initialPage': i, 'imgList': picList});
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(initialPage: i, imgList: picList);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
src: content.pictures[i]['img_src'],
|
||||
|
||||
@ -26,11 +26,6 @@ class VideoReplyReplyController extends GetxController {
|
||||
currentPage = 0;
|
||||
}
|
||||
|
||||
// 上拉加载
|
||||
Future onLoad() async {
|
||||
queryReplyList(type: 'onLoad');
|
||||
}
|
||||
|
||||
Future queryReplyList({type = 'init'}) async {
|
||||
if (type == 'init') {
|
||||
currentPage = 0;
|
||||
@ -49,11 +44,11 @@ class VideoReplyReplyController extends GetxController {
|
||||
if (replyList.length == res['data'].page.count) {
|
||||
noMore.value = '没有更多了';
|
||||
}
|
||||
currentPage++;
|
||||
} else {
|
||||
// 未登录状态replies可能返回null
|
||||
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
|
||||
}
|
||||
currentPage++;
|
||||
if (type == 'init') {
|
||||
// List<ReplyItemModel> replies = res['data'].replies;
|
||||
// 添加置顶回复
|
||||
@ -72,6 +67,10 @@ class VideoReplyReplyController extends GetxController {
|
||||
// res['data'].replies = replies;
|
||||
replyList.value = replies;
|
||||
} else {
|
||||
// 每次回复之后,翻页请求有且只有相同的一条回复数据
|
||||
if (replies.length == 1 && replies.last.rpid == replyList.last.rpid) {
|
||||
return;
|
||||
}
|
||||
replyList.addAll(replies);
|
||||
// res['data'].replies.addAll(replyList);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
@ -54,9 +55,9 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
if (!_videoReplyReplyController.isLoadingMore) {
|
||||
_videoReplyReplyController.onLoad();
|
||||
}
|
||||
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
|
||||
_videoReplyReplyController.queryReplyList(type: 'onLoad');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,25 +1,26 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/common/widgets/sliver_header.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/models/common/search_type.dart';
|
||||
import 'package:pilipala/pages/bangumi/introduction/index.dart';
|
||||
import 'package:pilipala/pages/danmaku/view.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/related/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
import 'widgets/app_bar.dart';
|
||||
import 'widgets/header_control.dart';
|
||||
|
||||
class VideoDetailPage extends StatefulWidget {
|
||||
@ -32,14 +33,14 @@ class VideoDetailPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
with TickerProviderStateMixin, RouteAware {
|
||||
final VideoDetailController videoDetailController =
|
||||
Get.put(VideoDetailController(), tag: Get.arguments['heroTag']);
|
||||
with TickerProviderStateMixin, RouteAware, WidgetsBindingObserver {
|
||||
late VideoDetailController videoDetailController;
|
||||
PlPlayerController? plPlayerController;
|
||||
final ScrollController _extendNestCtr = ScrollController();
|
||||
late StreamController<double> appbarStream;
|
||||
final VideoIntroController videoIntroController =
|
||||
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
|
||||
late VideoIntroController videoIntroController;
|
||||
late BangumiIntroController bangumiIntroController;
|
||||
late String heroTag;
|
||||
|
||||
PlayerStatus playerStatus = PlayerStatus.playing;
|
||||
double doubleOffset = 0;
|
||||
@ -51,15 +52,39 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
late Future _futureBuilderFuture;
|
||||
// 自动退出全屏
|
||||
late bool autoExitFullcreen;
|
||||
late bool autoPlayEnable;
|
||||
late bool autoPiP;
|
||||
final floating = Floating();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
|
||||
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
|
||||
videoIntroController.videoDetail.listen((value) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
value, videoDetailController.cid.value);
|
||||
});
|
||||
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
|
||||
bangumiIntroController.bangumiDetail.listen((value) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
value, videoDetailController.cid.value);
|
||||
});
|
||||
videoDetailController.cid.listen((p0) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
bangumiIntroController.bangumiDetail.value, p0);
|
||||
});
|
||||
statusBarHeight = localCache.get('statusBarHeight');
|
||||
autoExitFullcreen =
|
||||
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
|
||||
autoPlayEnable =
|
||||
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
|
||||
autoPiP = setting.get(SettingBoxKey.autoPiP, defaultValue: false);
|
||||
|
||||
videoSourceInit();
|
||||
appbarStreamListen();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
// 获取视频资源,初始化播放器
|
||||
@ -83,15 +108,38 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
}
|
||||
|
||||
// 播放器状态监听
|
||||
void playerListener(PlayerStatus? status) {
|
||||
void playerListener(PlayerStatus? status) async {
|
||||
playerStatus = status!;
|
||||
if (status == PlayerStatus.completed) {
|
||||
// 结束播放退出全屏
|
||||
if (autoExitFullcreen) {
|
||||
plPlayerController!.triggerFullScreen(status: false);
|
||||
}
|
||||
|
||||
/// 顺序播放 列表循环
|
||||
if (plPlayerController!.playRepeat != PlayRepeat.pause &&
|
||||
plPlayerController!.playRepeat != PlayRepeat.singleCycle) {
|
||||
if (videoDetailController.videoType == SearchType.video) {
|
||||
videoIntroController.nextPlay();
|
||||
}
|
||||
if (videoDetailController.videoType == SearchType.media_bangumi) {
|
||||
bangumiIntroController.nextPlay();
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个循环
|
||||
if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) {
|
||||
plPlayerController!.seekTo(Duration.zero);
|
||||
plPlayerController!.play();
|
||||
}
|
||||
// 播放完展示控制栏
|
||||
plPlayerController!.onLockControl(false);
|
||||
try {
|
||||
PiPStatus currentStatus =
|
||||
await videoDetailController.floating!.pipStatus;
|
||||
if (currentStatus == PiPStatus.disabled) {
|
||||
plPlayerController!.onLockControl(false);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +150,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
plPlayerController!.play();
|
||||
}
|
||||
|
||||
/// 未开启自动播放时触发播放
|
||||
Future<void> handlePlay() async {
|
||||
await videoDetailController.playerInit();
|
||||
plPlayerController = videoDetailController.plPlayerController;
|
||||
@ -111,8 +160,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.dispose();
|
||||
if (plPlayerController != null) {
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.dispose();
|
||||
}
|
||||
if (videoDetailController.floating != null) {
|
||||
videoDetailController.floating!.dispose();
|
||||
}
|
||||
videoPlayerServiceHandler.onVideoDetailDispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
floating.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -123,10 +180,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)) {
|
||||
videoDetailController.brightness = plPlayerController!.brightness.value;
|
||||
}
|
||||
videoDetailController.defaultST = plPlayerController!.position.value;
|
||||
videoIntroController.isPaused = true;
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.pause();
|
||||
if (plPlayerController != null) {
|
||||
videoDetailController.defaultST = plPlayerController!.position.value;
|
||||
videoIntroController.isPaused = true;
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.pause();
|
||||
}
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
@ -134,13 +193,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
// 返回当前页面时
|
||||
void didPopNext() async {
|
||||
videoDetailController.isFirstTime = false;
|
||||
videoDetailController.playerInit();
|
||||
bool autoplay = autoPlayEnable;
|
||||
videoDetailController.playerInit(autoplay: autoplay);
|
||||
|
||||
/// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回
|
||||
videoDetailController.autoPlay.value =
|
||||
!videoDetailController.isShowCover.value;
|
||||
videoIntroController.isPaused = false;
|
||||
if (_extendNestCtr.position.pixels == 0) {
|
||||
if (_extendNestCtr.position.pixels == 0 && autoplay) {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
plPlayerController!.play();
|
||||
plPlayerController!.seekTo(videoDetailController.defaultST);
|
||||
plPlayerController?.play();
|
||||
}
|
||||
plPlayerController!.addStatusLister(playerListener);
|
||||
plPlayerController?.addStatusLister(playerListener);
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
@ -151,12 +216,23 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState lifecycleState) {
|
||||
if (lifecycleState == AppLifecycleState.inactive && autoPiP) {
|
||||
floating.enable(
|
||||
aspectRatio: Rational(
|
||||
videoDetailController.data.dash!.video!.first.width!,
|
||||
videoDetailController.data.dash!.video!.first.height!,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
||||
final double pinnedHeaderHeight =
|
||||
statusBarHeight + kToolbarHeight + videoHeight;
|
||||
return SafeArea(
|
||||
Widget childWhenDisabled = SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Stack(
|
||||
@ -198,12 +274,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
? const SizedBox()
|
||||
: PLVideoPlayer(
|
||||
controller: plPlayerController!,
|
||||
headerControl: HeaderControl(
|
||||
controller:
|
||||
plPlayerController,
|
||||
videoDetailCtr:
|
||||
videoDetailController,
|
||||
),
|
||||
headerControl:
|
||||
videoDetailController
|
||||
.headerControl,
|
||||
danmuWidget: Obx(
|
||||
() => PlDanmaku(
|
||||
key: Key(
|
||||
@ -336,13 +409,18 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
];
|
||||
},
|
||||
// pinnedHeaderSliverHeightBuilder: () {
|
||||
// return playerStatus != PlayerStatus.playing
|
||||
// ? statusBarHeight + kToolbarHeight
|
||||
// : pinnedHeaderHeight;
|
||||
// },
|
||||
/// 不收回
|
||||
pinnedHeaderSliverHeightBuilder: () {
|
||||
return playerStatus != PlayerStatus.playing
|
||||
? statusBarHeight + kToolbarHeight
|
||||
: pinnedHeaderHeight;
|
||||
return pinnedHeaderHeight;
|
||||
},
|
||||
onlyOneScrollInBody: true,
|
||||
body: Container(
|
||||
key: Key(heroTag),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
@ -378,8 +456,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
const VideoIntroPanel(),
|
||||
] else if (videoDetailController.videoType ==
|
||||
SearchType.media_bangumi) ...[
|
||||
BangumiIntroPanel(
|
||||
cid: videoDetailController.cid)
|
||||
Obx(() => BangumiIntroPanel(
|
||||
cid: videoDetailController.cid.value)),
|
||||
],
|
||||
// if (videoDetailController.videoType ==
|
||||
// SearchType.video) ...[
|
||||
@ -418,21 +496,61 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// 重新进入会刷新
|
||||
// 播放完成/暂停播放
|
||||
StreamBuilder(
|
||||
stream: appbarStream.stream,
|
||||
initialData: 0,
|
||||
builder: ((context, snapshot) {
|
||||
return ScrollAppBar(
|
||||
snapshot.data!.toDouble(),
|
||||
() => continuePlay(),
|
||||
playerStatus,
|
||||
null,
|
||||
);
|
||||
}),
|
||||
)
|
||||
// StreamBuilder(
|
||||
// stream: appbarStream.stream,
|
||||
// initialData: 0,
|
||||
// builder: ((context, snapshot) {
|
||||
// return ScrollAppBar(
|
||||
// snapshot.data!.toDouble(),
|
||||
// () => continuePlay(),
|
||||
// playerStatus,
|
||||
// null,
|
||||
// );
|
||||
// }),
|
||||
// )
|
||||
],
|
||||
),
|
||||
);
|
||||
Widget childWhenEnabled = FutureBuilder(
|
||||
key: Key(heroTag),
|
||||
future: _futureBuilderFuture,
|
||||
builder: ((context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data['status']) {
|
||||
return Obx(
|
||||
() => !videoDetailController.autoPlay.value
|
||||
? const SizedBox()
|
||||
: PLVideoPlayer(
|
||||
controller: plPlayerController!,
|
||||
headerControl: HeaderControl(
|
||||
controller: plPlayerController,
|
||||
videoDetailCtr: videoDetailController,
|
||||
),
|
||||
danmuWidget: Obx(
|
||||
() => PlDanmaku(
|
||||
key: Key(
|
||||
videoDetailController.danmakuCid.value.toString()),
|
||||
cid: videoDetailController.danmakuCid.value,
|
||||
playerController: plPlayerController!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (Platform.isAndroid) {
|
||||
return PiPSwitcher(
|
||||
childWhenDisabled: childWhenDisabled,
|
||||
childWhenEnabled: childWhenEnabled,
|
||||
floating: floating,
|
||||
);
|
||||
} else {
|
||||
return childWhenDisabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
236
lib/pages/video/detail/widgets/ai_detail.dart
Normal file
236
lib/pages/video/detail/widgets/ai_detail.dart
Normal file
@ -0,0 +1,236 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/video/ai.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
Box localCache = GStrorage.localCache;
|
||||
late double sheetHeight;
|
||||
|
||||
class AiDetail extends StatelessWidget {
|
||||
final ModelResult? modelResult;
|
||||
|
||||
const AiDetail({
|
||||
Key? key,
|
||||
this.modelResult,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
sheetHeight = localCache.get('sheetHeight');
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
height: sheetHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
height: 35,
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
modelResult!.summary!,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: modelResult!.outline!.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
modelResult!.outline![index].title!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: modelResult!
|
||||
.outline![index].partOutline!.length,
|
||||
itemBuilder: (context, i) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground,
|
||||
height: 1.5,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: Utils.tampToSeektime(
|
||||
modelResult!
|
||||
.outline![index]
|
||||
.partOutline![i]
|
||||
.timestamp!),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 跳转到指定位置
|
||||
try {
|
||||
Get.find<VideoDetailController>(
|
||||
tag: Get.arguments[
|
||||
'heroTag'])
|
||||
.plPlayerController
|
||||
.seekTo(
|
||||
Duration(
|
||||
seconds:
|
||||
Utils.duration(
|
||||
Utils.tampToSeektime(modelResult!
|
||||
.outline![
|
||||
index]
|
||||
.partOutline![
|
||||
i]
|
||||
.timestamp!)
|
||||
.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
const TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: modelResult!
|
||||
.outline![index]
|
||||
.partOutline![i]
|
||||
.content!),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InlineSpan buildContent(BuildContext context, content) {
|
||||
List descV2 = content.descV2;
|
||||
// type
|
||||
// 1 普通文本
|
||||
// 2 @用户
|
||||
List<TextSpan> spanChilds = List.generate(descV2.length, (index) {
|
||||
final currentDesc = descV2[index];
|
||||
switch (currentDesc.type) {
|
||||
case 1:
|
||||
List<InlineSpan> spanChildren = [];
|
||||
RegExp urlRegExp = RegExp(r'https?://\S+\b');
|
||||
Iterable<Match> matches = urlRegExp.allMatches(currentDesc.rawText);
|
||||
|
||||
int previousEndIndex = 0;
|
||||
for (Match match in matches) {
|
||||
if (match.start > previousEndIndex) {
|
||||
spanChildren.add(TextSpan(
|
||||
text: currentDesc.rawText
|
||||
.substring(previousEndIndex, match.start)));
|
||||
}
|
||||
spanChildren.add(
|
||||
TextSpan(
|
||||
text: match.group(0),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 处理点击事件
|
||||
try {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': match.group(0)!,
|
||||
'type': 'url',
|
||||
'pageTitle': match.group(0)!,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
previousEndIndex = match.end;
|
||||
}
|
||||
|
||||
if (previousEndIndex < currentDesc.rawText.length) {
|
||||
spanChildren.add(TextSpan(
|
||||
text: currentDesc.rawText.substring(previousEndIndex)));
|
||||
}
|
||||
|
||||
TextSpan result = TextSpan(children: spanChildren);
|
||||
return result;
|
||||
case 2:
|
||||
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
|
||||
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
|
||||
return TextSpan(
|
||||
text: '@${currentDesc.rawText}',
|
||||
style: TextStyle(color: colorSchemePrimary),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
Get.toNamed(
|
||||
'/member?mid=${currentDesc.bizId}',
|
||||
arguments: {'face': '', 'heroTag': heroTag},
|
||||
);
|
||||
},
|
||||
);
|
||||
default:
|
||||
return const TextSpan();
|
||||
}
|
||||
});
|
||||
return TextSpan(children: spanChilds);
|
||||
}
|
||||
}
|
||||
@ -48,15 +48,15 @@ class ScrollAppBar extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.share,
|
||||
size: 20,
|
||||
)),
|
||||
const SizedBox(width: 12)
|
||||
],
|
||||
// actions: [
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(
|
||||
// Icons.share,
|
||||
// size: 20,
|
||||
// )),
|
||||
// const SizedBox(width: 12)
|
||||
// ],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,18 +1,29 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ns_danmaku/ns_danmaku.dart';
|
||||
import 'package:pilipala/models/video/play/quality.dart';
|
||||
import 'package:pilipala/models/video/play/url.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
|
||||
final PlPlayerController? controller;
|
||||
final VideoDetailController? videoDetailCtr;
|
||||
final Floating? floating;
|
||||
const HeaderControl({
|
||||
this.controller,
|
||||
this.videoDetailCtr,
|
||||
this.floating,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -29,11 +40,16 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
TextStyle subTitleStyle = const TextStyle(fontSize: 12);
|
||||
TextStyle titleStyle = const TextStyle(fontSize: 14);
|
||||
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
|
||||
Box localCache = GStrorage.localCache;
|
||||
Box videoStorage = GStrorage.video;
|
||||
late List speedsList;
|
||||
double buttonSpace = 8;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
videoInfo = widget.videoDetailCtr!.data;
|
||||
speedsList = widget.controller!.speedsList;
|
||||
}
|
||||
|
||||
/// 设置面板
|
||||
@ -45,7 +61,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
builder: (_) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 400,
|
||||
height: 440,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
@ -73,7 +89,6 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {},
|
||||
@ -138,17 +153,17 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
|
||||
style: subTitleStyle),
|
||||
),
|
||||
// ListTile(
|
||||
// onTap: () {},
|
||||
// dense: true,
|
||||
// enabled: false,
|
||||
// leading: const Icon(Icons.play_circle_outline, size: 20),
|
||||
// title: Text('播放设置', style: titleStyle),
|
||||
// ),
|
||||
ListTile(
|
||||
onTap: () {},
|
||||
onTap: () => {Get.back(), showSetRepeat()},
|
||||
dense: true,
|
||||
leading: const Icon(Icons.repeat, size: 20),
|
||||
title: Text('播放顺序', style: titleStyle),
|
||||
subtitle: Text(widget.controller!.playRepeat.description,
|
||||
style: subTitleStyle),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => {Get.back(), showSetDanmaku()},
|
||||
dense: true,
|
||||
enabled: false,
|
||||
leading: const Icon(Icons.subtitles_outlined, size: 20),
|
||||
title: Text('弹幕设置', style: titleStyle),
|
||||
),
|
||||
@ -167,26 +182,38 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
/// 选择倍速
|
||||
void showSetSpeedSheet() {
|
||||
double currentSpeed = widget.controller!.playbackSpeed;
|
||||
SmartDialog.show(
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('播放速度'),
|
||||
contentPadding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
|
||||
content: StatefulBuilder(builder: (context, StateSetter setState) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
spacing: 8,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
Text('$currentSpeed倍'),
|
||||
Slider(
|
||||
min: PlaySpeed.values.first.value,
|
||||
max: PlaySpeed.values.last.value,
|
||||
value: currentSpeed,
|
||||
divisions: PlaySpeed.values.length - 1,
|
||||
label: '${currentSpeed}x',
|
||||
onChanged: (double val) =>
|
||||
{setState(() => currentSpeed = val)},
|
||||
)
|
||||
for (var i in speedsList) ...[
|
||||
if (i == currentSpeed) ...[
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
// setState(() => currentSpeed = i),
|
||||
await widget.controller!.setPlaybackSpeed(i);
|
||||
Get.back();
|
||||
},
|
||||
child: Text(i.toString()),
|
||||
),
|
||||
] else ...[
|
||||
FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
// setState(() => currentSpeed = i),
|
||||
await widget.controller!.setPlaybackSpeed(i);
|
||||
Get.back();
|
||||
},
|
||||
child: Text(i.toString()),
|
||||
),
|
||||
]
|
||||
]
|
||||
],
|
||||
);
|
||||
}),
|
||||
@ -200,10 +227,10 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await SmartDialog.dismiss();
|
||||
widget.controller!.setPlaybackSpeed(currentSpeed);
|
||||
await widget.controller!.setDefaultSpeed();
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('确定'),
|
||||
child: const Text('默认速度'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -257,7 +284,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('选择画质', style: titleStyle),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
@ -454,6 +481,300 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 弹幕功能
|
||||
void showSetDanmaku() async {
|
||||
// 屏蔽类型
|
||||
List<Map<String, dynamic>> blockTypesList = [
|
||||
{'value': 5, 'label': '顶部'},
|
||||
{'value': 2, 'label': '滚动'},
|
||||
{'value': 4, 'label': '底部'},
|
||||
{'value': 6, 'label': '彩色'},
|
||||
];
|
||||
List blockTypes = widget.controller!.blockTypes;
|
||||
// 显示区域
|
||||
List<Map<String, dynamic>> showAreas = [
|
||||
{'value': 0.25, 'label': '1/4屏'},
|
||||
{'value': 0.5, 'label': '半屏'},
|
||||
{'value': 0.75, 'label': '3/4屏'},
|
||||
{'value': 1.0, 'label': '满屏'},
|
||||
];
|
||||
double showArea = widget.controller!.showArea;
|
||||
// 不透明度
|
||||
double opacityVal = widget.controller!.opacityVal;
|
||||
// 字体大小
|
||||
double fontSizeVal = widget.controller!.fontSizeVal;
|
||||
// 弹幕速度
|
||||
double danmakuSpeedVal = widget.controller!.danmakuSpeedVal;
|
||||
|
||||
DanmakuController danmakuController = widget.controller!.danmakuController!;
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(builder: (context, StateSetter setState) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 580,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 45,
|
||||
child: Center(child: Text('弹幕设置', style: titleStyle)),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text('按类型屏蔽'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 18),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i in blockTypesList) ...[
|
||||
ActionRowLineItem(
|
||||
onTap: () async {
|
||||
bool isChoose = blockTypes.contains(i['value']);
|
||||
if (isChoose) {
|
||||
blockTypes.remove(i['value']);
|
||||
} else {
|
||||
blockTypes.add(i['value']);
|
||||
}
|
||||
widget.controller!.blockTypes = blockTypes;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(
|
||||
hideTop: blockTypes.contains(5),
|
||||
hideBottom: blockTypes.contains(4),
|
||||
hideScroll: blockTypes.contains(2),
|
||||
// 添加或修改其他需要修改的选项属性
|
||||
);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
text: i['label'],
|
||||
selectStatus: blockTypes.contains(i['value']),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text('显示区域'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 18),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i in showAreas) ...[
|
||||
ActionRowLineItem(
|
||||
onTap: () {
|
||||
showArea = i['value'];
|
||||
widget.controller!.showArea = showArea;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(area: i['value']);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
text: i['label'],
|
||||
selectStatus: showArea == i['value'],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
Text('不透明度 ${opacityVal * 100}%'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0,
|
||||
max: 1,
|
||||
value: opacityVal,
|
||||
divisions: 10,
|
||||
label: '${opacityVal * 100}%',
|
||||
onChanged: (double val) {
|
||||
opacityVal = val;
|
||||
widget.controller!.opacityVal = opacityVal;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(opacity: val);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0.5,
|
||||
max: 2.5,
|
||||
value: fontSizeVal,
|
||||
divisions: 20,
|
||||
label: '${(fontSizeVal * 100).toStringAsFixed(1)}%',
|
||||
onChanged: (double val) {
|
||||
fontSizeVal = val;
|
||||
widget.controller!.fontSizeVal = fontSizeVal;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(
|
||||
fontSize: (15 * fontSizeVal).toDouble(),
|
||||
);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('弹幕时长 ${danmakuSpeedVal.toString()}'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 1,
|
||||
max: 8,
|
||||
value: danmakuSpeedVal,
|
||||
divisions: 14,
|
||||
label: danmakuSpeedVal.toString(),
|
||||
onChanged: (double val) {
|
||||
danmakuSpeedVal = val;
|
||||
widget.controller!.danmakuSpeedVal = danmakuSpeedVal;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(duration: val);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 播放顺序
|
||||
void showSetRepeat() async {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 250,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 45,
|
||||
child: Center(child: Text('选择播放顺序', style: titleStyle))),
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: ListView(
|
||||
children: [
|
||||
for (var i in PlayRepeat.values) ...[
|
||||
ListTile(
|
||||
onTap: () {
|
||||
widget.controller!.setPlayRepeat(i);
|
||||
Get.back();
|
||||
},
|
||||
dense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.only(left: 20, right: 20),
|
||||
title: Text(i.description),
|
||||
trailing: widget.controller!.playRepeat == i
|
||||
? Icon(
|
||||
Icons.done,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: const SizedBox(),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _ = widget.controller!;
|
||||
@ -480,7 +801,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
fuc: () => Get.back(),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
ComBtn(
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.house,
|
||||
@ -525,7 +846,40 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
if (Platform.isAndroid) ...[
|
||||
SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () async {
|
||||
bool canUsePiP = false;
|
||||
widget.controller!.hiddenControls(false);
|
||||
try {
|
||||
canUsePiP = await widget.floating!.isPipAvailable;
|
||||
} on PlatformException catch (_) {
|
||||
canUsePiP = false;
|
||||
}
|
||||
if (canUsePiP) {
|
||||
final aspectRatio = Rational(
|
||||
widget.videoDetailCtr!.data.dash!.video!.first.width!,
|
||||
widget.videoDetailCtr!.data.dash!.video!.first.height!,
|
||||
);
|
||||
await widget.floating!.enable(aspectRatio: aspectRatio);
|
||||
} else {}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.picture_in_picture_outlined,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: buttonSpace),
|
||||
],
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
width: 45,
|
||||
@ -542,7 +896,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
ComBtn(
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.sliders,
|
||||
@ -556,3 +910,21 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MSliderTrackShape extends RoundedRectSliderTrackShape {
|
||||
@override
|
||||
Rect getPreferredRect({
|
||||
required RenderBox parentBox,
|
||||
Offset offset = Offset.zero,
|
||||
SliderThemeData? sliderTheme,
|
||||
bool isEnabled = false,
|
||||
bool isDiscrete = false,
|
||||
}) {
|
||||
const double trackHeight = 3;
|
||||
final double trackLeft = offset.dx;
|
||||
final double trackTop =
|
||||
offset.dy + (parentBox.size.height - trackHeight) / 2 + 4;
|
||||
final double trackWidth = parentBox.size.width;
|
||||
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user