Merge branch 'main' into feature-opusRender

This commit is contained in:
guozhigq
2024-12-01 23:13:21 +08:00
13 changed files with 346 additions and 253 deletions

View File

@ -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,

View File

@ -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();
}); });
} }

View File

@ -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(

View File

@ -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,

View File

@ -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 =

View File

@ -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!

View File

@ -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(),
), ),
], ],
), ),

View File

@ -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();

View File

@ -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,

View File

@ -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),
], ],
), ),

View 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');
},
);
});
}
}

View File

@ -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:

View File

@ -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: