Merge branch 'main' into feature-opusRender
This commit is contained in:
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/common/constants.dart';
|
import 'package:pilipala/common/constants.dart';
|
||||||
@ -183,7 +182,7 @@ class _PagesBottomSheetState extends State<PagesBottomSheet>
|
|||||||
isFixedHeight: true,
|
isFixedHeight: true,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_listObserverControllerList![widget.currentEpisodeIndex!]
|
_listObserverControllerList![widget.currentEpisodeIndex ?? 0]
|
||||||
.initialIndexModel = ObserverIndexPositionModel(
|
.initialIndexModel = ObserverIndexPositionModel(
|
||||||
index: currentIndex,
|
index: currentIndex,
|
||||||
isFixedHeight: true,
|
isFixedHeight: true,
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import '../../../models/video/reply/item.dart';
|
|||||||
import '../widgets/dynamic_panel.dart';
|
import '../widgets/dynamic_panel.dart';
|
||||||
|
|
||||||
class DynamicDetailPage extends StatefulWidget {
|
class DynamicDetailPage extends StatefulWidget {
|
||||||
// const DynamicDetailPage({super.key});
|
|
||||||
const DynamicDetailPage({Key? key}) : super(key: key);
|
const DynamicDetailPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -90,19 +89,22 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
|||||||
_dynamicDetailController = Get.put(
|
_dynamicDetailController = Get.put(
|
||||||
DynamicDetailController(oid, replyType),
|
DynamicDetailController(oid, replyType),
|
||||||
tag: opusId.toString());
|
tag: opusId.toString());
|
||||||
|
_futureBuilderFuture = _dynamicDetailController.queryReplyList();
|
||||||
await _dynamicDetailController.reqHtmlByOpusId(opusId!);
|
await _dynamicDetailController.reqHtmlByOpusId(opusId!);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
oid = moduleDynamic.major!.draw!.id!;
|
oid = moduleDynamic.major!.draw!.id!;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (err) {
|
||||||
|
print('err:${err.toString()}');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!isOpusId) {
|
if (!isOpusId) {
|
||||||
_dynamicDetailController =
|
_dynamicDetailController =
|
||||||
Get.put(DynamicDetailController(oid, replyType), tag: oid.toString());
|
Get.put(DynamicDetailController(oid, replyType), tag: oid.toString());
|
||||||
|
_futureBuilderFuture ??= _dynamicDetailController.queryReplyList();
|
||||||
}
|
}
|
||||||
_futureBuilderFuture = _dynamicDetailController.queryReplyList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看二级评论
|
// 查看二级评论
|
||||||
@ -132,7 +134,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
|||||||
// 分页加载
|
// 分页加载
|
||||||
if (scrollController.position.pixels >=
|
if (scrollController.position.pixels >=
|
||||||
scrollController.position.maxScrollExtent - 300) {
|
scrollController.position.maxScrollExtent - 300) {
|
||||||
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
|
EasyThrottle.throttle('replyList', const Duration(seconds: 2), () {
|
||||||
_dynamicDetailController.onLoad();
|
_dynamicDetailController.onLoad();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -617,6 +617,7 @@ class VideoIntroController extends GetxController {
|
|||||||
// 播放器底栏 选集 回调
|
// 播放器底栏 选集 回调
|
||||||
void showEposideHandler() {
|
void showEposideHandler() {
|
||||||
late List episodes;
|
late List episodes;
|
||||||
|
int currentEpisodeIndex = 0;
|
||||||
VideoEpidoesType dataType = VideoEpidoesType.videoEpisode;
|
VideoEpidoesType dataType = VideoEpidoesType.videoEpisode;
|
||||||
if (videoDetail.value.ugcSeason != null) {
|
if (videoDetail.value.ugcSeason != null) {
|
||||||
dataType = VideoEpidoesType.videoEpisode;
|
dataType = VideoEpidoesType.videoEpisode;
|
||||||
@ -625,6 +626,7 @@ class VideoIntroController extends GetxController {
|
|||||||
final List<EpisodeItem> episodesList = sections[i].episodes!;
|
final List<EpisodeItem> episodesList = sections[i].episodes!;
|
||||||
for (int j = 0; j < episodesList.length; j++) {
|
for (int j = 0; j < episodesList.length; j++) {
|
||||||
if (episodesList[j].cid == lastPlayCid.value) {
|
if (episodesList[j].cid == lastPlayCid.value) {
|
||||||
|
currentEpisodeIndex = i;
|
||||||
episodes = episodesList;
|
episodes = episodesList;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -644,6 +646,7 @@ class VideoIntroController extends GetxController {
|
|||||||
sheetHeight: Get.size.height,
|
sheetHeight: Get.size.height,
|
||||||
isFullScreen: true,
|
isFullScreen: true,
|
||||||
ugcSeason: ugcSeason,
|
ugcSeason: ugcSeason,
|
||||||
|
currentEpisodeIndex: currentEpisodeIndex,
|
||||||
changeFucCall: (item, index) {
|
changeFucCall: (item, index) {
|
||||||
if (dataType == VideoEpidoesType.videoEpisode) {
|
if (dataType == VideoEpidoesType.videoEpisode) {
|
||||||
changeSeasonOrbangu(
|
changeSeasonOrbangu(
|
||||||
|
|||||||
@ -42,23 +42,14 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
|||||||
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
|
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
|
||||||
|
|
||||||
/// 根据 cid 找到对应集,找到对应 episodes
|
/// 根据 cid 找到对应集,找到对应 episodes
|
||||||
final List<SectionItem> sections = widget.ugcSeason.sections!;
|
getCurrentEpisodeIndex();
|
||||||
for (int i = 0; i < sections.length; i++) {
|
|
||||||
final List<EpisodeItem> episodesList = sections[i].episodes!;
|
|
||||||
for (int j = 0; j < episodesList.length; j++) {
|
|
||||||
if (episodesList[j].cid == cid) {
|
|
||||||
currentEpisodeIndex = i;
|
|
||||||
episodes = episodesList;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 取对应 season_id 的 episodes
|
/// 取对应 season_id 的 episodes
|
||||||
getCurrentIndex();
|
getCurrentIndex();
|
||||||
_videoDetailController.cid.listen((int p0) {
|
_videoDetailController.cid.listen((int p0) {
|
||||||
cid = p0;
|
cid = p0;
|
||||||
getCurrentIndex();
|
getCurrentIndex();
|
||||||
|
getCurrentEpisodeIndex();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +85,21 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取currentEpisodeIndex
|
||||||
|
void getCurrentEpisodeIndex() {
|
||||||
|
final List<SectionItem> sections = widget.ugcSeason.sections!;
|
||||||
|
for (int i = 0; i < sections.length; i++) {
|
||||||
|
final List<EpisodeItem> episodesList = sections[i].episodes!;
|
||||||
|
for (int j = 0; j < episodesList.length; j++) {
|
||||||
|
if (episodesList[j].cid == cid) {
|
||||||
|
currentEpisodeIndex = i;
|
||||||
|
episodes = episodesList;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildEpisodeListItem(
|
Widget buildEpisodeListItem(
|
||||||
EpisodeItem episode,
|
EpisodeItem episode,
|
||||||
int index,
|
int index,
|
||||||
|
|||||||
@ -70,6 +70,13 @@ class VideoReplyController extends GetxController {
|
|||||||
isEnd = res['data'].cursor.isEnd ?? false;
|
isEnd = res['data'].cursor.isEnd ?? false;
|
||||||
nextOffset = res['data'].cursor.paginationReply.nextOffset ?? "";
|
nextOffset = res['data'].cursor.paginationReply.nextOffset ?? "";
|
||||||
if (replies.isNotEmpty) {
|
if (replies.isNotEmpty) {
|
||||||
|
/// 临时修复
|
||||||
|
final bool flag = replyList
|
||||||
|
.any((ReplyItemModel reply) => reply.rpid == replies.first.rpid);
|
||||||
|
if (replies.length == 1 && flag) {
|
||||||
|
replies.clear();
|
||||||
|
isEnd = true;
|
||||||
|
}
|
||||||
noMore.value = isEnd ? '没有更多了' : '加载中...';
|
noMore.value = isEnd ? '没有更多了' : '加载中...';
|
||||||
} else {
|
} else {
|
||||||
noMore.value =
|
noMore.value =
|
||||||
|
|||||||
@ -685,6 +685,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
canPop:
|
canPop:
|
||||||
plPlayerController?.isFullScreen.value != true,
|
plPlayerController?.isFullScreen.value != true,
|
||||||
onPopInvoked: (bool didPop) {
|
onPopInvoked: (bool didPop) {
|
||||||
|
if (plPlayerController?.controlsLock.value ==
|
||||||
|
true) {
|
||||||
|
plPlayerController?.onLockControl(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (plPlayerController?.isFullScreen.value ==
|
if (plPlayerController?.isFullScreen.value ==
|
||||||
true) {
|
true) {
|
||||||
plPlayerController!
|
plPlayerController!
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:marquee/marquee.dart';
|
||||||
import 'package:ns_danmaku/ns_danmaku.dart';
|
import 'package:ns_danmaku/ns_danmaku.dart';
|
||||||
import 'package:pilipala/common/widgets/drag_handle.dart';
|
import 'package:pilipala/common/widgets/drag_handle.dart';
|
||||||
import 'package:pilipala/http/user.dart';
|
import 'package:pilipala/http/user.dart';
|
||||||
@ -62,6 +63,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
late String heroTag;
|
late String heroTag;
|
||||||
late VideoIntroController videoIntroController;
|
late VideoIntroController videoIntroController;
|
||||||
late VideoDetailData videoDetail;
|
late VideoDetailData videoDetail;
|
||||||
|
DateTime initialTime = DateTime.now();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -108,30 +110,6 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
child: Material(
|
child: Material(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
// ListTile(
|
|
||||||
// onTap: () {},
|
|
||||||
// dense: true,
|
|
||||||
// enabled: false,
|
|
||||||
// leading:
|
|
||||||
// const Icon(Icons.network_cell_outlined, size: 20),
|
|
||||||
// title: Text('省流模式', style: titleStyle),
|
|
||||||
// subtitle: Text('低画质 | 减少视频缓存', style: subTitleStyle),
|
|
||||||
// trailing: Transform.scale(
|
|
||||||
// scale: 0.75,
|
|
||||||
// child: Switch(
|
|
||||||
// thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
|
|
||||||
// (Set<MaterialState> states) {
|
|
||||||
// if (states.isNotEmpty &&
|
|
||||||
// states.first == MaterialState.selected) {
|
|
||||||
// return const Icon(Icons.done);
|
|
||||||
// }
|
|
||||||
// return null; // All other states will use the default thumbIcon.
|
|
||||||
// }),
|
|
||||||
// value: false,
|
|
||||||
// onChanged: (value) => {},
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final res = await UserHttp.toViewLater(
|
final res = await UserHttp.toViewLater(
|
||||||
@ -1071,6 +1049,16 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<DateTime> _getTimeStream() {
|
||||||
|
return Stream.periodic(const Duration(seconds: 60), (count) {
|
||||||
|
return DateTime.now();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTime(DateTime dateTime) {
|
||||||
|
return '${dateTime.hour}:${dateTime.minute < 10 ? '0${dateTime.minute}' : dateTime.minute}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final _ = widget.controller!;
|
final _ = widget.controller!;
|
||||||
@ -1086,49 +1074,12 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
primary: false,
|
primary: false,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
titleSpacing: 14,
|
titleSpacing: 14,
|
||||||
title: Row(
|
title: Column(
|
||||||
children: [
|
children: [
|
||||||
ComBtn(
|
if (isFullScreen.value && isLandscape) ...[
|
||||||
icon: const Icon(
|
Row(
|
||||||
FontAwesomeIcons.arrowLeft,
|
|
||||||
size: 15,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
fuc: () => <Set<void>>{
|
|
||||||
if (widget.controller!.isFullScreen.value)
|
|
||||||
<void>{widget.controller!.triggerFullScreen(status: false)}
|
|
||||||
else
|
|
||||||
<void>{
|
|
||||||
if (MediaQuery.of(context).orientation ==
|
|
||||||
Orientation.landscape)
|
|
||||||
{
|
|
||||||
SystemChrome.setPreferredOrientations([
|
|
||||||
DeviceOrientation.portraitUp,
|
|
||||||
])
|
|
||||||
},
|
|
||||||
Get.back()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(width: buttonSpace),
|
|
||||||
if (isFullScreen.value &&
|
|
||||||
isLandscape &&
|
|
||||||
widget.videoType == SearchType.video) ...[
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
const SizedBox(width: 40),
|
||||||
constraints: const BoxConstraints(maxWidth: 200),
|
|
||||||
child: Obx(
|
|
||||||
() => Text(
|
|
||||||
videoIntroController.videoDetail.value.title ?? '',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (videoIntroController.isShowOnlineTotal)
|
if (videoIntroController.isShowOnlineTotal)
|
||||||
Text(
|
Text(
|
||||||
'${videoIntroController.total.value}人正在看',
|
'${videoIntroController.total.value}人正在看',
|
||||||
@ -1136,138 +1087,226 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: StreamBuilder<DateTime>(
|
||||||
|
stream: _getTimeStream(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
String currentTime = _formatTime(snapshot.data!);
|
||||||
|
return Text(
|
||||||
|
currentTime,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
);
|
||||||
|
} else if (snapshot.connectionState ==
|
||||||
|
ConnectionState.waiting) {
|
||||||
|
// 如果Stream还未发出数据,先显示初始获取的时间
|
||||||
|
String currentTime = _formatTime(initialTime);
|
||||||
|
return Text(
|
||||||
|
currentTime,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
|
||||||
|
/// TODO 网络&电量
|
||||||
],
|
],
|
||||||
)
|
|
||||||
] else ...[
|
|
||||||
ComBtn(
|
|
||||||
icon: const Icon(
|
|
||||||
FontAwesomeIcons.house,
|
|
||||||
size: 15,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
fuc: () async {
|
|
||||||
// 销毁播放器实例
|
|
||||||
await widget.controller!.dispose(type: 'all');
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.popUntil(
|
|
||||||
context, (Route<dynamic> route) => route.isFirst);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const Spacer(),
|
Row(
|
||||||
if (GlobalDataCache.enableDlna) ...[
|
children: [
|
||||||
ComBtn(
|
ComBtn(
|
||||||
icon: Image.asset('assets/images/video/dlna.png', width: 19),
|
icon: const Icon(
|
||||||
fuc: () async {
|
FontAwesomeIcons.arrowLeft,
|
||||||
showDialog<void>(
|
size: 15,
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return LiveDlnaPage(
|
|
||||||
datasource: widget.videoDetailCtr!.videoUrl);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(width: buttonSpace),
|
|
||||||
],
|
|
||||||
|
|
||||||
/// 弹幕开关(全屏时)
|
|
||||||
if (isFullScreen.value) ...[
|
|
||||||
SizedBox(
|
|
||||||
width: 56,
|
|
||||||
height: 34,
|
|
||||||
child: TextButton(
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
|
||||||
),
|
|
||||||
onPressed: () => showShootDanmakuSheet(),
|
|
||||||
child: const Text(
|
|
||||||
'发弹幕',
|
|
||||||
style: textStyle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: buttonSpace),
|
|
||||||
SizedBox(
|
|
||||||
width: 34,
|
|
||||||
height: 34,
|
|
||||||
child: Obx(
|
|
||||||
() => IconButton(
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
_.isOpenDanmu.value = !_.isOpenDanmu.value;
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
_.isOpenDanmu.value
|
|
||||||
? Icons.subtitles_outlined
|
|
||||||
: Icons.subtitles_off_outlined,
|
|
||||||
size: 19,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: buttonSpace),
|
|
||||||
],
|
|
||||||
|
|
||||||
/// pip
|
|
||||||
if (Platform.isAndroid) ...<Widget>[
|
|
||||||
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 Rational aspectRatio = Rational(
|
|
||||||
widget.videoDetailCtr!.data.dash!.video!.first.width!,
|
|
||||||
widget.videoDetailCtr!.data.dash!.video!.first.height!,
|
|
||||||
);
|
|
||||||
await widget.floating!.enable(aspectRatio: aspectRatio);
|
|
||||||
} else {}
|
|
||||||
},
|
|
||||||
icon: Image.asset(
|
|
||||||
'assets/images/video/pip.png',
|
|
||||||
width: 19,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
|
fuc: () => <Set<void>>{
|
||||||
|
if (widget.controller!.isFullScreen.value)
|
||||||
|
<void>{widget.controller!.triggerFullScreen(status: false)}
|
||||||
|
else
|
||||||
|
<void>{
|
||||||
|
if (MediaQuery.of(context).orientation ==
|
||||||
|
Orientation.landscape)
|
||||||
|
{
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
])
|
||||||
|
},
|
||||||
|
Get.back()
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: buttonSpace),
|
||||||
SizedBox(width: buttonSpace),
|
if (isFullScreen.value &&
|
||||||
],
|
isLandscape &&
|
||||||
|
widget.videoType == SearchType.video) ...[
|
||||||
|
Expanded(
|
||||||
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
|
return SizedBox(
|
||||||
|
width: constraints.maxWidth,
|
||||||
|
height: 25,
|
||||||
|
child: Obx(
|
||||||
|
() => Marquee(
|
||||||
|
text: videoIntroController.videoDetail.value.title ??
|
||||||
|
'',
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
scrollAxis: Axis.horizontal,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
blankSpace: constraints.maxWidth,
|
||||||
|
velocity: 100,
|
||||||
|
pauseAfterRound: const Duration(seconds: 1),
|
||||||
|
startPadding: 0,
|
||||||
|
accelerationDuration: const Duration(seconds: 1),
|
||||||
|
accelerationCurve: Curves.linear,
|
||||||
|
decelerationDuration: const Duration(seconds: 1),
|
||||||
|
decelerationCurve: Curves.easeOut,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
ComBtn(
|
||||||
|
icon: const Icon(
|
||||||
|
FontAwesomeIcons.house,
|
||||||
|
size: 15,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
fuc: () async {
|
||||||
|
// 销毁播放器实例
|
||||||
|
await widget.controller!.dispose(type: 'all');
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.popUntil(
|
||||||
|
context, (Route<dynamic> route) => route.isFirst);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
if (GlobalDataCache.enableDlna) ...[
|
||||||
|
ComBtn(
|
||||||
|
icon: Image.asset('assets/images/video/dlna.png', width: 19),
|
||||||
|
fuc: () async {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return LiveDlnaPage(
|
||||||
|
datasource: widget.videoDetailCtr!.videoUrl);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(width: buttonSpace),
|
||||||
|
],
|
||||||
|
|
||||||
/// 字幕
|
/// 弹幕开关(全屏时)
|
||||||
if (widget.showSubtitleBtn) ...[
|
if (isFullScreen.value) ...[
|
||||||
ComBtn(
|
SizedBox(
|
||||||
icon: Icon(
|
width: 56,
|
||||||
FontAwesomeIcons.closedCaptioning,
|
height: 34,
|
||||||
size: 16,
|
child: TextButton(
|
||||||
color: Colors.white.withOpacity(0.9),
|
style: ButtonStyle(
|
||||||
|
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
|
),
|
||||||
|
onPressed: () => showShootDanmakuSheet(),
|
||||||
|
child: const Text(
|
||||||
|
'发弹幕',
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: buttonSpace),
|
||||||
|
SizedBox(
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
child: Obx(
|
||||||
|
() => IconButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
_.isOpenDanmu.value = !_.isOpenDanmu.value;
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
_.isOpenDanmu.value
|
||||||
|
? Icons.subtitles_outlined
|
||||||
|
: Icons.subtitles_off_outlined,
|
||||||
|
size: 19,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: buttonSpace),
|
||||||
|
],
|
||||||
|
|
||||||
|
/// pip
|
||||||
|
if (Platform.isAndroid) ...<Widget>[
|
||||||
|
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 Rational aspectRatio = Rational(
|
||||||
|
widget.videoDetailCtr!.data.dash!.video!.first.width!,
|
||||||
|
widget
|
||||||
|
.videoDetailCtr!.data.dash!.video!.first.height!,
|
||||||
|
);
|
||||||
|
await widget.floating!.enable(aspectRatio: aspectRatio);
|
||||||
|
} else {}
|
||||||
|
},
|
||||||
|
icon: Image.asset(
|
||||||
|
'assets/images/video/pip.png',
|
||||||
|
width: 19,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: buttonSpace),
|
||||||
|
],
|
||||||
|
|
||||||
|
/// 字幕
|
||||||
|
if (widget.showSubtitleBtn) ...[
|
||||||
|
ComBtn(
|
||||||
|
icon: Icon(
|
||||||
|
FontAwesomeIcons.closedCaptioning,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
fuc: () => showSubtitleDialog(),
|
||||||
|
),
|
||||||
|
SizedBox(width: buttonSpace),
|
||||||
|
],
|
||||||
|
ComBtn(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.more_vert_outlined,
|
||||||
|
size: 19,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
fuc: () => showSettingSheet(),
|
||||||
),
|
),
|
||||||
fuc: () => showSubtitleDialog(),
|
],
|
||||||
),
|
|
||||||
SizedBox(width: buttonSpace),
|
|
||||||
],
|
|
||||||
ComBtn(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.more_vert_outlined,
|
|
||||||
size: 19,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
fuc: () => showSettingSheet(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -917,7 +917,7 @@ class PlPlayerController {
|
|||||||
if (videoType == 'live') {
|
if (videoType == 'live') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (controlsLock.value) {
|
if (_controlsLock.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_doubleSpeedStatus.value = val;
|
_doubleSpeedStatus.value = val;
|
||||||
@ -1093,6 +1093,7 @@ class PlPlayerController {
|
|||||||
videoFitChangedTimer?.cancel();
|
videoFitChangedTimer?.cancel();
|
||||||
// _position.close();
|
// _position.close();
|
||||||
_playerEventSubs?.cancel();
|
_playerEventSubs?.cancel();
|
||||||
|
_controlsLock.value = false;
|
||||||
// _sliderPosition.close();
|
// _sliderPosition.close();
|
||||||
// _sliderTempPosition.close();
|
// _sliderTempPosition.close();
|
||||||
// _isSliderMoving.close();
|
// _isSliderMoving.close();
|
||||||
|
|||||||
@ -842,8 +842,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
total: Duration(seconds: max),
|
total: Duration(seconds: max),
|
||||||
progressBarColor: colorTheme,
|
progressBarColor: colorTheme,
|
||||||
baseBarColor: Colors.white.withOpacity(0.2),
|
baseBarColor: Colors.white.withOpacity(0.2),
|
||||||
bufferedBarColor:
|
bufferedBarColor: Colors.white.withOpacity(0.6),
|
||||||
Theme.of(context).colorScheme.primary.withOpacity(0.4),
|
|
||||||
timeLabelLocation: TimeLabelLocation.none,
|
timeLabelLocation: TimeLabelLocation.none,
|
||||||
thumbColor: colorTheme,
|
thumbColor: colorTheme,
|
||||||
barHeight: 3,
|
barHeight: 3,
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'progress_bar.dart';
|
||||||
|
|
||||||
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final PlPlayerController? controller;
|
final PlPlayerController? controller;
|
||||||
@ -20,54 +18,18 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Color colorTheme = Theme.of(context).colorScheme.primary;
|
|
||||||
final _ = controller!;
|
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
height: 90,
|
height: 90,
|
||||||
padding: const EdgeInsets.only(left: 18, right: 18),
|
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Obx(
|
Padding(
|
||||||
() {
|
padding: const EdgeInsets.fromLTRB(7, 0, 7, 6),
|
||||||
final int value = _.sliderPositionSeconds.value;
|
child: ProgressBarWidget(controller: controller!),
|
||||||
final int max = _.durationSeconds.value;
|
|
||||||
final int buffer = _.bufferedSeconds.value;
|
|
||||||
if (value > max || max <= 0) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 7, right: 7, bottom: 6),
|
|
||||||
child: ProgressBar(
|
|
||||||
progress: Duration(seconds: value),
|
|
||||||
buffered: Duration(seconds: buffer),
|
|
||||||
total: Duration(seconds: max),
|
|
||||||
progressBarColor: colorTheme,
|
|
||||||
baseBarColor: Colors.white.withOpacity(0.2),
|
|
||||||
bufferedBarColor: colorTheme.withOpacity(0.4),
|
|
||||||
timeLabelLocation: TimeLabelLocation.none,
|
|
||||||
thumbColor: colorTheme,
|
|
||||||
barHeight: 3.5,
|
|
||||||
thumbRadius: 7,
|
|
||||||
onDragStart: (duration) {
|
|
||||||
feedBack();
|
|
||||||
_.onChangedSliderStart();
|
|
||||||
},
|
|
||||||
onDragUpdate: (duration) {
|
|
||||||
_.onUpdatedSliderProgress(duration.timeStamp);
|
|
||||||
},
|
|
||||||
onSeek: (duration) {
|
|
||||||
_.onChangedSliderEnd();
|
|
||||||
_.onChangedSlider(duration.inSeconds.toDouble());
|
|
||||||
_.seekTo(Duration(seconds: duration.inSeconds),
|
|
||||||
type: 'slider');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Row(children: [...buildBottomControl!]),
|
Row(children: buildBottomControl!),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
52
lib/plugin/pl_player/widgets/progress_bar.dart
Normal file
52
lib/plugin/pl_player/widgets/progress_bar.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
|
|
||||||
|
class ProgressBarWidget extends StatelessWidget {
|
||||||
|
final PlPlayerController controller;
|
||||||
|
|
||||||
|
const ProgressBarWidget({
|
||||||
|
required this.controller,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
Color colorTheme = Theme.of(context).colorScheme.primary;
|
||||||
|
final _ = controller;
|
||||||
|
final int value = _.sliderPositionSeconds.value;
|
||||||
|
final int max = _.durationSeconds.value;
|
||||||
|
final int buffer = _.bufferedSeconds.value;
|
||||||
|
if (value > max || max <= 0) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
return ProgressBar(
|
||||||
|
progress: Duration(seconds: value),
|
||||||
|
buffered: Duration(seconds: buffer),
|
||||||
|
total: Duration(seconds: max),
|
||||||
|
progressBarColor: colorTheme,
|
||||||
|
baseBarColor: Colors.white.withOpacity(0.2),
|
||||||
|
bufferedBarColor: Colors.white.withOpacity(0.6),
|
||||||
|
timeLabelLocation: TimeLabelLocation.none,
|
||||||
|
thumbColor: colorTheme,
|
||||||
|
barHeight: 3.5,
|
||||||
|
thumbRadius: 7,
|
||||||
|
onDragStart: (duration) {
|
||||||
|
feedBack();
|
||||||
|
_.onChangedSliderStart();
|
||||||
|
},
|
||||||
|
onDragUpdate: (duration) {
|
||||||
|
_.onUpdatedSliderProgress(duration.timeStamp);
|
||||||
|
},
|
||||||
|
onSeek: (duration) {
|
||||||
|
_.onChangedSliderEnd();
|
||||||
|
_.onChangedSlider(duration.inSeconds.toDouble());
|
||||||
|
_.seekTo(Duration(seconds: duration.inSeconds), type: 'slider');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pubspec.lock
16
pubspec.lock
@ -497,6 +497,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.1"
|
version: "6.2.1"
|
||||||
|
fading_edge_scrollview:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fading_edge_scrollview
|
||||||
|
sha256: c25c2231652ce774cc31824d0112f11f653881f43d7f5302c05af11942052031
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -990,6 +998,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
version: "6.1.0"
|
||||||
|
marquee:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: marquee
|
||||||
|
sha256: "4b5243d2804373bdc25fc93d42c3b402d6ec1f4ee8d0bb72276edd04ae7addb8"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.3"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -152,6 +152,8 @@ dependencies:
|
|||||||
re_highlight: ^0.0.3
|
re_highlight: ^0.0.3
|
||||||
# 图片选择器
|
# 图片选择器
|
||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
|
# 跑马灯
|
||||||
|
marquee: ^2.2.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user