Compare commits

..

19 Commits

Author SHA1 Message Date
4dbcd2e0ec fix: 尝试修复音量调节抖动 issues #647 #498 #104 #198 2024-03-21 23:39:56 +08:00
00b81b194f Merge branch 'fix-issues#620' 2024-03-20 00:13:51 +08:00
af96d16062 feat: 电池优化 2024-03-17 14:45:40 +08:00
12c299685b Merge branch 'feature-removeLockIcon' 2024-03-17 14:35:14 +08:00
1182a58cb4 Merge branch 'fix' 2024-03-17 14:30:26 +08:00
e04a7e5702 fix: 图片保存命名、质量、权限问题 2024-03-17 14:29:39 +08:00
e9dc6f7fdb Merge branch 'main' into fix 2024-03-17 01:13:54 +08:00
25d1ccc87a Merge branch 'feature-liveRoomRender' 2024-03-17 00:38:19 +08:00
33d28f51d1 feat: 未登录状态切换直播画质提示 2024-03-17 00:36:00 +08:00
a37f3b8b5b Merge branch 'main' into feature-liveRoomRender 2024-03-16 23:35:18 +08:00
a57f5e8b2f Merge branch 'main' of github.com:guozhigq/pilipala 2024-03-16 23:12:52 +08:00
beb640ac83 Merge branch 'fix-bangumi' 2024-03-16 23:12:31 +08:00
fca0588377 Merge pull request #628 from guozhigq/feature-customBottomControl
Feature custom bottom control
2024-03-16 23:08:22 +08:00
b13d7b475b feat: 非全屏状态下隐藏锁定按钮 2024-03-14 23:49:36 +08:00
da2bbeedff mod: 默认直播画质设置 2024-03-13 23:12:34 +08:00
98aaca286b feat: 直播画质切换 2024-03-11 00:02:11 +08:00
6fdfcb888d feat: 播放器底部控制栏自定义 2024-03-10 19:41:23 +08:00
f41bb02bae fix: 合集最后一p不显示 issues #620 2024-03-08 23:05:09 +08:00
105a29f311 feat: disable Battery Optimization 2024-03-08 22:56:16 +08:00
15 changed files with 574 additions and 218 deletions

View File

@ -16,6 +16,7 @@ import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart';
import 'package:pilipala/pages/main/view.dart';
import 'package:pilipala/services/disable_battery_opt.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart';
@ -71,6 +72,7 @@ void main() async {
));
Data.init();
PiliSchame.init();
DisableBatteryOpt();
});
}

View File

@ -0,0 +1,43 @@
enum LiveQuality {
dolby,
super4K,
origin,
bluRay,
superHD,
smooth,
flunt,
}
extension LiveQualityCode on LiveQuality {
static final List<int> _codeList = [
30000,
20000,
10000,
400,
250,
150,
80,
];
int get code => _codeList[index];
static LiveQuality? fromCode(int code) {
final index = _codeList.indexOf(code);
if (index != -1) {
return LiveQuality.values[index];
}
return null;
}
}
extension VideoQualityDesc on LiveQuality {
static final List<String> _descList = [
'杜比',
'4K',
'原画',
'蓝光',
'超清',
'高清',
'流畅',
];
get description => _descList[index];
}

View File

@ -148,9 +148,9 @@ class _BangumiPanelState extends State<BangumiPanel> {
Expanded(
child: Material(
child: ScrollablePositionedList.builder(
itemCount: widget.pages.length,
itemCount: widget.pages.length + 1,
itemBuilder: (BuildContext context, int index) {
bool isLastItem = index == widget.pages.length - 1;
bool isLastItem = index == widget.pages.length;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(

View File

@ -1,6 +1,8 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/quality.dart';
import 'package:pilipala/models/live/room_info.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import '../../models/live/room_info_h5.dart';
@ -19,10 +21,16 @@ class LiveRoomController extends GetxController {
PlPlayerController.getInstance(videoType: 'live');
Rx<RoomInfoH5Model> roomInfoH5 = RoomInfoH5Model().obs;
late bool enableCDN;
late int currentQn;
int? tempCurrentQn;
late List<Map<String, dynamic>> acceptQnList;
RxString currentQnDesc = ''.obs;
@override
void onInit() {
super.onInit();
currentQn = setting.get(SettingBoxKey.defaultLiveQa,
defaultValue: LiveQuality.values.last.code);
roomId = int.parse(Get.parameters['roomid']!);
if (Get.arguments != null) {
liveItem = Get.arguments['liveItem'];
@ -57,11 +65,28 @@ class LiveRoomController extends GetxController {
}
Future queryLiveInfo() async {
var res = await LiveHttp.liveRoomInfo(roomId: roomId, qn: 10000);
var res = await LiveHttp.liveRoomInfo(roomId: roomId, qn: currentQn);
if (res['status']) {
List<CodecItem> codec =
res['data'].playurlInfo.playurl.stream.first.format.first.codec;
CodecItem item = codec.first;
// 以服务端返回的码率为准
currentQn = item.currentQn!;
if (tempCurrentQn != null && tempCurrentQn == currentQn) {
SmartDialog.showToast('画质切换失败,请检查登录状态');
}
List acceptQn = item.acceptQn!;
acceptQnList = acceptQn.map((e) {
return {
'code': e,
'desc': LiveQuality.values
.firstWhere((element) => element.code == e)
.description,
};
}).toList();
currentQnDesc.value = LiveQuality.values
.firstWhere((element) => element.code == currentQn)
.description;
String videoUrl = enableCDN
? VideoUtils.getCdnUrl(item)
: (item.urlInfo?.first.host)! +
@ -90,4 +115,17 @@ class LiveRoomController extends GetxController {
}
return res;
}
// 修改画质
void changeQn(int qn) async {
tempCurrentQn = currentQn;
if (currentQn == qn) {
return;
}
currentQn = qn;
currentQnDesc.value = LiveQuality.values
.firstWhere((element) => element.code == currentQn)
.description;
await queryLiveInfo();
}
}

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/pages/live_room/index.dart';
@ -83,6 +84,30 @@ class _BottomControlState extends State<BottomControl> {
// ),
// ),
// const SizedBox(width: 4),
SizedBox(
width: 30,
child: PopupMenuButton<int>(
padding: EdgeInsets.zero,
onSelected: (value) {
widget.liveRoomCtr!.changeQn(value);
},
child: Obx(
() => Text(
widget.liveRoomCtr!.currentQnDesc.value,
style: const TextStyle(color: Colors.white, fontSize: 13),
),
),
itemBuilder: (BuildContext context) {
return widget.liveRoomCtr!.acceptQnList.map((e) {
return PopupMenuItem<int>(
value: e['code'],
child: Text(e['desc']),
);
}).toList();
},
),
),
const SizedBox(width: 10),
if (Platform.isAndroid) ...[
SizedBox(
width: 34,
@ -110,7 +135,7 @@ class _BottomControlState extends State<BottomControl> {
),
),
),
const SizedBox(width: 4),
const SizedBox(width: 10),
],
ComBtn(
icon: const Icon(

View File

@ -10,6 +10,7 @@ import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/global_data.dart';
import 'package:pilipala/utils/storage.dart';
import '../../models/live/quality.dart';
import 'widgets/switch_item.dart';
class PlaySetting extends StatefulWidget {
@ -22,6 +23,7 @@ class PlaySetting extends StatefulWidget {
class _PlaySettingState extends State<PlaySetting> {
Box setting = GStrorage.setting;
late dynamic defaultVideoQa;
late dynamic defaultLiveQa;
late dynamic defaultAudioQa;
late dynamic defaultDecode;
late int defaultFullScreenMode;
@ -32,6 +34,8 @@ class _PlaySettingState extends State<PlaySetting> {
super.initState();
defaultVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
defaultValue: VideoQuality.values.last.code);
defaultLiveQa = setting.get(SettingBoxKey.defaultLiveQa,
defaultValue: LiveQuality.values.last.code);
defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.values.last.code);
defaultDecode = setting.get(SettingBoxKey.defaultDecode,
@ -157,9 +161,9 @@ class _PlaySettingState extends State<PlaySetting> {
}),
ListTile(
dense: false,
title: Text('默认画质', style: titleStyle),
title: Text('默认视频画质', style: titleStyle),
subtitle: Text(
'当前画质${VideoQualityCode.fromCode(defaultVideoQa)!.description!}',
'当前默认画质${VideoQualityCode.fromCode(defaultVideoQa)!.description!}',
style: subTitleStyle,
),
onTap: () async {
@ -167,7 +171,7 @@ class _PlaySettingState extends State<PlaySetting> {
context: context,
builder: (context) {
return SelectDialog<int>(
title: '默认画质',
title: '默认视频画质',
value: defaultVideoQa,
values: VideoQuality.values.reversed.map((e) {
return {'title': e.description, 'value': e.code};
@ -181,6 +185,32 @@ class _PlaySettingState extends State<PlaySetting> {
}
},
),
ListTile(
dense: false,
title: Text('默认直播画质', style: titleStyle),
subtitle: Text(
'当前默认画质${LiveQualityCode.fromCode(defaultLiveQa)!.description!}',
style: subTitleStyle,
),
onTap: () async {
int? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<int>(
title: '默认直播画质',
value: defaultLiveQa,
values: LiveQuality.values.reversed.map((e) {
return {'title': e.description, 'value': e.code};
}).toList());
},
);
if (result != null) {
defaultLiveQa = result;
setting.put(SettingBoxKey.defaultLiveQa, result);
setState(() {});
}
},
),
ListTile(
dense: false,
title: Text('默认音质', style: titleStyle),

View File

@ -161,9 +161,9 @@ class _SeasonPanelState extends State<SeasonPanel> {
Expanded(
child: Material(
child: ScrollablePositionedList.builder(
itemCount: episodes.length,
itemCount: episodes.length + 1,
itemBuilder: (BuildContext context, int index) {
bool isLastItem = index == episodes.length - 1;
bool isLastItem = index == episodes.length;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(

View File

@ -0,0 +1,10 @@
enum BottomControlType {
pre,
playOrPause,
next,
time,
space,
fit,
speed,
fullscreen,
}

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -8,7 +9,6 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:nil/nil.dart';
import 'package:pilipala/models/common/gesture_mode.dart';
import 'package:pilipala/plugin/pl_player/controller.dart';
import 'package:pilipala/plugin/pl_player/models/duration.dart';
@ -19,12 +19,14 @@ import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import '../../utils/global_data.dart';
import 'models/bottom_control_type.dart';
import 'models/bottom_progress_behavior.dart';
import 'widgets/app_bar_ani.dart';
import 'widgets/backward_seek.dart';
import 'widgets/bottom_control.dart';
import 'widgets/common_btn.dart';
import 'widgets/forward_seek.dart';
import 'widgets/play_pause_btn.dart';
class PLVideoPlayer extends StatefulWidget {
const PLVideoPlayer({
@ -32,6 +34,7 @@ class PLVideoPlayer extends StatefulWidget {
this.headerControl,
this.bottomControl,
this.danmuWidget,
this.bottomList,
super.key,
});
@ -39,6 +42,7 @@ class PLVideoPlayer extends StatefulWidget {
final PreferredSizeWidget? headerControl;
final PreferredSizeWidget? bottomControl;
final Widget? danmuWidget;
final List<BottomControlType>? bottomList;
@override
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
@ -48,26 +52,22 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
with TickerProviderStateMixin {
late AnimationController animationController;
late VideoController videoController;
final PLVideoPlayerController _ctr = Get.put(PLVideoPlayerController());
// bool _mountSeekBackwardButton = false;
// bool _mountSeekForwardButton = false;
// bool _hideSeekBackwardButton = false;
// bool _hideSeekForwardButton = false;
final RxBool _mountSeekBackwardButton = false.obs;
final RxBool _mountSeekForwardButton = false.obs;
final RxBool _hideSeekBackwardButton = false.obs;
final RxBool _hideSeekForwardButton = false.obs;
// double _brightnessValue = 0.0;
// bool _brightnessIndicator = false;
final RxDouble _brightnessValue = 0.0.obs;
final RxBool _brightnessIndicator = false.obs;
Timer? _brightnessTimer;
// double _volumeValue = 0.0;
// bool _volumeIndicator = false;
final RxDouble _volumeValue = 0.0.obs;
final RxBool _volumeIndicator = false.obs;
Timer? _volumeTimer;
double _distance = 0.0;
// 初始手指落下位置
// double _initTapPositoin = 0.0;
// bool _volumeInterceptEventStream = false;
final RxDouble _distance = 0.0.obs;
final RxBool _volumeInterceptEventStream = false.obs;
Box setting = GStrorage.setting;
late FullScreenMode mode;
@ -82,11 +82,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
DateTime? lastFullScreenToggleTime;
void onDoubleTapSeekBackward() {
_ctr.onDoubleTapSeekBackward();
_mountSeekBackwardButton.value = true;
}
void onDoubleTapSeekForward() {
_ctr.onDoubleTapSeekForward();
_mountSeekForwardButton.value = true;
}
// 双击播放、暂停
@ -138,10 +138,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Future.microtask(() async {
try {
FlutterVolumeController.updateShowSystemUI(true);
_ctr.volumeValue.value = (await FlutterVolumeController.getVolume())!;
_volumeValue.value = (await FlutterVolumeController.getVolume())!;
FlutterVolumeController.addListener((double value) {
if (mounted && !_ctr.volumeInterceptEventStream.value) {
_ctr.volumeValue.value = value;
if (mounted && !_volumeInterceptEventStream.value) {
_volumeValue.value = value;
}
});
} catch (_) {}
@ -149,10 +149,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Future.microtask(() async {
try {
_ctr.brightnessValue.value = await ScreenBrightness().current;
_brightnessValue.value = await ScreenBrightness().current;
ScreenBrightness().onCurrentBrightnessChanged.listen((double value) {
if (mounted) {
_ctr.brightnessValue.value = value;
_brightnessValue.value = value;
}
});
} catch (_) {}
@ -164,14 +164,14 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
FlutterVolumeController.updateShowSystemUI(false);
await FlutterVolumeController.setVolume(value);
} catch (_) {}
_ctr.volumeValue.value = value;
_ctr.volumeIndicator.value = true;
_ctr.volumeInterceptEventStream.value = true;
_volumeValue.value = value;
_volumeIndicator.value = true;
_volumeInterceptEventStream.value = true;
_volumeTimer?.cancel();
_volumeTimer = Timer(const Duration(milliseconds: 200), () {
if (mounted) {
_ctr.volumeIndicator.value = false;
_ctr.volumeInterceptEventStream.value = false;
_volumeIndicator.value = false;
_volumeInterceptEventStream.value = false;
}
});
}
@ -180,11 +180,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
try {
await ScreenBrightness().setScreenBrightness(value);
} catch (_) {}
_ctr.brightnessIndicator.value = true;
_brightnessIndicator.value = true;
_brightnessTimer?.cancel();
_brightnessTimer = Timer(const Duration(milliseconds: 200), () {
if (mounted) {
_ctr.brightnessIndicator.value = false;
_brightnessIndicator.value = false;
}
});
widget.controller.brightness.value = value;
@ -197,6 +197,134 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
super.dispose();
}
// 动态构建底部控制条
List<Widget> buildBottomControl() {
const TextStyle textStyle = TextStyle(
color: Colors.white,
fontSize: 12,
);
final PlPlayerController _ = widget.controller;
Map<BottomControlType, Widget> videoProgressWidgets = {
/// 上一集
BottomControlType.pre: ComBtn(
icon: const Icon(
Icons.skip_previous_outlined,
size: 15,
color: Colors.white,
),
fuc: () {},
),
/// 播放暂停
BottomControlType.playOrPause: PlayOrPauseButton(
controller: _,
),
/// 下一集
BottomControlType.next: ComBtn(
icon: const Icon(
Icons.last_page_outlined,
size: 15,
color: Colors.white,
),
fuc: () {},
),
/// 时间进度
BottomControlType.time: Row(
children: [
Obx(() {
return Text(
_.durationSeconds.value >= 3600
? printDurationWithHours(
Duration(seconds: _.positionSeconds.value))
: printDuration(Duration(seconds: _.positionSeconds.value)),
style: textStyle,
);
}),
const SizedBox(width: 2),
const Text('/', style: textStyle),
const SizedBox(width: 2),
Obx(
() => Text(
_.durationSeconds.value >= 3600
? printDurationWithHours(
Duration(seconds: _.durationSeconds.value))
: printDuration(Duration(seconds: _.durationSeconds.value)),
style: textStyle,
),
),
],
),
/// 空白占位
BottomControlType.space: const Spacer(),
/// 画面比例
BottomControlType.fit: SizedBox(
height: 30,
child: TextButton(
onPressed: () => _.toggleVideoFit(),
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
child: Obx(
() => Text(
_.videoFitDEsc.value,
style: const TextStyle(color: Colors.white, fontSize: 13),
),
),
),
),
/// 播放速度
BottomControlType.speed: SizedBox(
width: 45,
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {},
child: Obx(
() => Text(
'${_.playbackSpeed.toString()}X',
style: textStyle,
),
),
),
),
/// 字幕
/// 全屏
BottomControlType.fullscreen: ComBtn(
icon: Obx(
() => Icon(
_.isFullScreen.value
? FontAwesomeIcons.compress
: FontAwesomeIcons.expand,
size: 15,
color: Colors.white,
),
),
fuc: () => _.triggerFullScreen(),
),
};
final List<Widget> list = [];
var userSpecifyItem = widget.bottomList ??
[
BottomControlType.playOrPause,
BottomControlType.time,
BottomControlType.space,
BottomControlType.fit,
BottomControlType.fullscreen,
];
for (var i = 0; i < userSpecifyItem.length; i++) {
list.add(videoProgressWidgets[userSpecifyItem[i]]!);
}
return list;
}
@override
Widget build(BuildContext context) {
final PlPlayerController _ = widget.controller;
@ -316,7 +444,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
() => Align(
child: AnimatedOpacity(
curve: Curves.easeInOut,
opacity: _ctr.volumeIndicator.value ? 1.0 : 0.0,
opacity: _volumeIndicator.value ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
child: Container(
alignment: Alignment.center,
@ -335,9 +463,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
width: 28.0,
alignment: Alignment.centerRight,
child: Icon(
_ctr.volumeValue.value == 0.0
_volumeValue.value == 0.0
? Icons.volume_off
: _ctr.volumeValue.value < 0.5
: _volumeValue.value < 0.5
? Icons.volume_down
: Icons.volume_up,
color: const Color(0xFFFFFFFF),
@ -346,7 +474,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
Expanded(
child: Text(
'${(_ctr.volumeValue.value * 100.0).round()}%',
'${(_volumeValue.value * 100.0).round()}%',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 13.0,
@ -367,7 +495,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
() => Align(
child: AnimatedOpacity(
curve: Curves.easeInOut,
opacity: _ctr.brightnessIndicator.value ? 1.0 : 0.0,
opacity: _brightnessIndicator.value ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
child: Container(
alignment: Alignment.center,
@ -386,9 +514,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
width: 28.0,
alignment: Alignment.centerRight,
child: Icon(
_ctr.brightnessValue.value < 1.0 / 3.0
_brightnessValue.value < 1.0 / 3.0
? Icons.brightness_low
: _ctr.brightnessValue.value < 2.0 / 3.0
: _brightnessValue.value < 2.0 / 3.0
? Icons.brightness_medium
: Icons.brightness_high,
color: const Color(0xFFFFFFFF),
@ -398,7 +526,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
const SizedBox(width: 2.0),
Expanded(
child: Text(
'${(_ctr.brightnessValue.value * 100.0).round()}%',
'${(_brightnessValue.value * 100.0).round()}%',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 13.0,
@ -489,7 +617,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
pos.clamp(Duration.zero, _.duration.value);
_.onUpdatedSliderProgress(result);
_.onChangedSliderStart();
// _initTapPositoin = tapPosition;
},
onHorizontalDragEnd: (DragEndDetails details) {
if (_.videoType.value == 'live' || _.controlsLock.value) {
@ -521,7 +648,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
: screenWidth * 9 / 16) *
3;
final double brightness =
_ctr.brightnessValue.value - delta / level;
_brightnessValue.value - delta / level;
final double result = brightness.clamp(0.0, 1.0);
setBrightness(result);
} else if (tapPosition < sectionWidth * 2) {
@ -530,31 +657,34 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
const double threshold = 7.0; // 滑动阈值
final bool flag =
fullScreenGestureMode != FullScreenGestureMode.values.last;
if (dy > _distance && dy > threshold) {
if (dy > _distance.value && dy > threshold) {
if (_.isFullScreen.value ^ flag) {
lastFullScreenToggleTime = DateTime.now();
// 下滑退出全屏
await widget.controller.triggerFullScreen(status: flag);
}
_distance = 0.0;
} else if (dy < _distance && dy < -threshold) {
_distance.value = 0.0;
} else if (dy < _distance.value && dy < -threshold) {
if (!_.isFullScreen.value ^ flag) {
lastFullScreenToggleTime = DateTime.now();
// 上滑进入全屏
await widget.controller.triggerFullScreen(status: !flag);
}
_distance = 0.0;
_distance.value = 0.0;
}
_distance = dy;
_distance.value = dy;
} else {
// 右边区域 👈
final double level = (_.isFullScreen.value
? Get.size.height
: screenWidth * 9 / 16) *
3;
final double volume = _ctr.volumeValue.value - delta / level;
final double result = volume.clamp(0.0, 1.0);
setVolume(result);
EasyThrottle.throttle(
'setVolume', const Duration(milliseconds: 20), () {
final double level = (_.isFullScreen.value
? Get.size.height
: screenWidth * 9 / 16);
final double volume = _volumeValue.value -
double.parse(delta.toStringAsFixed(1)) / level;
final double result = volume.clamp(0.0, 1.0);
setVolume(result);
});
}
},
onVerticalDragEnd: (DragEndDetails details) {},
@ -585,9 +715,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
position: 'bottom',
child: widget.bottomControl ??
BottomControl(
controller: widget.controller,
triggerFullScreen:
widget.controller.triggerFullScreen),
controller: widget.controller,
triggerFullScreen: _.triggerFullScreen,
buildBottomControl: buildBottomControl(),
),
),
),
],
@ -607,23 +738,23 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}
if (defaultBtmProgressBehavior ==
BtmProgresBehavior.alwaysHide.code) {
return nil;
return const SizedBox();
}
if (defaultBtmProgressBehavior ==
BtmProgresBehavior.onlyShowFullScreen.code &&
!_.isFullScreen.value) {
return nil;
return const SizedBox();
} else if (defaultBtmProgressBehavior ==
BtmProgresBehavior.onlyHideFullScreen.code &&
_.isFullScreen.value) {
return nil;
return const SizedBox();
}
if (_.videoType.value == 'live') {
return const SizedBox();
}
if (value > max || max <= 0) {
return nil;
return const SizedBox();
}
return Positioned(
bottom: -1.5,
@ -672,7 +803,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 锁
Obx(
() => Visibility(
visible: _.videoType.value != 'live',
visible: _.videoType.value != 'live' && _.isFullScreen.value,
child: Align(
alignment: Alignment.centerLeft,
child: FractionalTranslation(
@ -720,18 +851,17 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 点击 快进/快退
Obx(
() => Visibility(
visible: _ctr.mountSeekBackwardButton.value ||
_ctr.mountSeekForwardButton.value,
visible:
_mountSeekBackwardButton.value || _mountSeekForwardButton.value,
child: Positioned.fill(
child: Row(
children: [
Expanded(
child: _ctr.mountSeekBackwardButton.value
child: _mountSeekBackwardButton.value
? TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0.0,
end:
_ctr.hideSeekBackwardButton.value ? 0.0 : 1.0,
end: _hideSeekBackwardButton.value ? 0.0 : 1.0,
),
duration: const Duration(milliseconds: 500),
builder: (BuildContext context, double value,
@ -741,17 +871,15 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
child: child,
),
onEnd: () {
if (_ctr.hideSeekBackwardButton.value) {
_ctr.hideSeekBackwardButton.value = false;
_ctr.mountSeekBackwardButton.value = false;
if (_hideSeekBackwardButton.value) {
_hideSeekBackwardButton.value = false;
_mountSeekBackwardButton.value = false;
}
},
child: BackwardSeekIndicator(
onChanged: (Duration value) {
// _seekBarDeltaValueNotifier.value = -value;
},
onChanged: (Duration value) => {},
onSubmitted: (Duration value) {
_ctr.hideSeekBackwardButton.value = true;
_hideSeekBackwardButton.value = true;
final Player player =
widget.controller.videoPlayerController!;
Duration result = player.state.position - value;
@ -764,7 +892,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
},
),
)
: nil,
: const SizedBox(),
),
Expanded(
child: SizedBox(
@ -772,11 +900,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
),
Expanded(
child: _ctr.mountSeekForwardButton.value
child: _mountSeekForwardButton.value
? TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0.0,
end: _ctr.hideSeekForwardButton.value ? 0.0 : 1.0,
end: _hideSeekForwardButton.value ? 0.0 : 1.0,
),
duration: const Duration(milliseconds: 500),
builder: (BuildContext context, double value,
@ -786,17 +914,15 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
child: child,
),
onEnd: () {
if (_ctr.hideSeekForwardButton.value) {
_ctr.hideSeekForwardButton.value = false;
_ctr.mountSeekForwardButton.value = false;
if (_hideSeekForwardButton.value) {
_hideSeekForwardButton.value = false;
_mountSeekForwardButton.value = false;
}
},
child: ForwardSeekIndicator(
onChanged: (Duration value) {
// _seekBarDeltaValueNotifier.value = value;
},
onChanged: (Duration value) => {},
onSubmitted: (Duration value) {
_ctr.hideSeekForwardButton.value = true;
_hideSeekForwardButton.value = true;
final Player player =
widget.controller.videoPlayerController!;
Duration result = player.state.position + value;
@ -809,7 +935,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
},
),
)
: nil,
: const SizedBox(),
),
],
),
@ -820,31 +946,3 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
);
}
}
class PLVideoPlayerController extends GetxController {
RxBool mountSeekBackwardButton = false.obs;
RxBool mountSeekForwardButton = false.obs;
RxBool hideSeekBackwardButton = false.obs;
RxBool hideSeekForwardButton = false.obs;
RxDouble brightnessValue = 0.0.obs;
RxBool brightnessIndicator = false.obs;
RxDouble volumeValue = 0.0.obs;
RxBool volumeIndicator = false.obs;
RxDouble distance = 0.0.obs;
// 初始手指落下位置
RxDouble initTapPositoin = 0.0.obs;
RxBool volumeInterceptEventStream = false.obs;
// 双击快进 展示样式
void onDoubleTapSeekForward() {
mountSeekForwardButton.value = true;
}
void onDoubleTapSeekBackward() {
mountSeekBackwardButton.value = true;
}
}

View File

@ -1,17 +1,20 @@
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:nil/nil.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/widgets/play_pause_btn.dart';
import 'package:pilipala/utils/feed_back.dart';
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
final Function? triggerFullScreen;
const BottomControl({this.controller, this.triggerFullScreen, Key? key})
: super(key: key);
final List<Widget>? buildBottomControl;
const BottomControl({
this.controller,
this.triggerFullScreen,
this.buildBottomControl,
Key? key,
}) : super(key: key);
@override
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@ -20,11 +23,6 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
Widget build(BuildContext context) {
Color colorTheme = Theme.of(context).colorScheme.primary;
final _ = controller!;
const textStyle = TextStyle(
color: Colors.white,
fontSize: 12,
);
return Container(
color: Colors.transparent,
height: 90,
@ -71,86 +69,89 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
},
),
Row(
children: [
PlayOrPauseButton(
controller: _,
),
const SizedBox(width: 4),
// 播放时间
Obx(() {
return Text(
_.durationSeconds.value >= 3600
? printDurationWithHours(
Duration(seconds: _.positionSeconds.value))
: printDuration(
Duration(seconds: _.positionSeconds.value)),
style: textStyle,
);
}),
const SizedBox(width: 2),
const Text('/', style: textStyle),
const SizedBox(width: 2),
Obx(
() => Text(
_.durationSeconds.value >= 3600
? printDurationWithHours(
Duration(seconds: _.durationSeconds.value))
: printDuration(
Duration(seconds: _.durationSeconds.value)),
style: textStyle,
),
),
const Spacer(),
// 倍速
// Obx(
// () => SizedBox(
// width: 45,
// height: 34,
// child: TextButton(
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// onPressed: () {
// _.togglePlaybackSpeed();
// },
// child: Text(
// '${_.playbackSpeed.toString()}X',
// style: textStyle,
// ),
// ),
// ),
// ),
SizedBox(
height: 30,
child: TextButton(
onPressed: () => _.toggleVideoFit(),
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
child: Obx(
() => Text(
_.videoFitDEsc.value,
style: const TextStyle(color: Colors.white, fontSize: 13),
),
),
),
),
const SizedBox(width: 10),
// 全屏
Obx(
() => ComBtn(
icon: Icon(
_.isFullScreen.value
? FontAwesomeIcons.compress
: FontAwesomeIcons.expand,
size: 15,
color: Colors.white,
),
fuc: () => triggerFullScreen!(),
),
),
],
children: [...buildBottomControl!],
),
// Row(
// children: [
// PlayOrPauseButton(
// controller: _,
// ),
// const SizedBox(width: 4),
// // 播放时间
// Obx(() {
// return Text(
// _.durationSeconds.value >= 3600
// ? printDurationWithHours(
// Duration(seconds: _.positionSeconds.value))
// : printDuration(
// Duration(seconds: _.positionSeconds.value)),
// style: textStyle,
// );
// }),
// const SizedBox(width: 2),
// const Text('/', style: textStyle),
// const SizedBox(width: 2),
// Obx(
// () => Text(
// _.durationSeconds.value >= 3600
// ? printDurationWithHours(
// Duration(seconds: _.durationSeconds.value))
// : printDuration(
// Duration(seconds: _.durationSeconds.value)),
// style: textStyle,
// ),
// ),
// const Spacer(),
// // 倍速
// // Obx(
// // () => SizedBox(
// // width: 45,
// // height: 34,
// // child: TextButton(
// // style: ButtonStyle(
// // padding: MaterialStateProperty.all(EdgeInsets.zero),
// // ),
// // onPressed: () {
// // _.togglePlaybackSpeed();
// // },
// // child: Text(
// // '${_.playbackSpeed.toString()}X',
// // style: textStyle,
// // ),
// // ),
// // ),
// // ),
// SizedBox(
// height: 30,
// child: TextButton(
// onPressed: () => _.toggleVideoFit(),
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// child: Obx(
// () => Text(
// _.videoFitDEsc.value,
// style: const TextStyle(color: Colors.white, fontSize: 13),
// ),
// ),
// ),
// ),
// const SizedBox(width: 10),
// // 全屏
// Obx(
// () => ComBtn(
// icon: Icon(
// _.isFullScreen.value
// ? FontAwesomeIcons.compress
// : FontAwesomeIcons.expand,
// size: 15,
// color: Colors.white,
// ),
// fuc: () => triggerFullScreen!(),
// ),
// ),
// ],
// ),
const SizedBox(height: 12),
],
),

View File

@ -0,0 +1,40 @@
import 'dart:io';
import 'package:disable_battery_optimization/disable_battery_optimization.dart';
import 'package:pilipala/utils/storage.dart';
void DisableBatteryOpt() async {
if (!Platform.isAndroid) {
return;
}
// 本地缓存中读取 是否禁用了电池优化 默认未禁用
bool isDisableBatteryOptLocal =
GStrorage.localCache.get('isDisableBatteryOptLocal', defaultValue: false);
if (!isDisableBatteryOptLocal) {
final isBatteryOptimizationDisabled =
await DisableBatteryOptimization.isBatteryOptimizationDisabled;
if (isBatteryOptimizationDisabled == false) {
final hasDisabled = await DisableBatteryOptimization
.showDisableBatteryOptimizationSettings();
// 设置为已禁用
GStrorage.localCache.put('isDisableBatteryOptLocal', hasDisabled == true);
}
}
bool isManufacturerBatteryOptimizationDisabled = GStrorage.localCache
.get('isManufacturerBatteryOptimizationDisabled', defaultValue: false);
if (!isManufacturerBatteryOptimizationDisabled) {
final isManBatteryOptimizationDisabled = await DisableBatteryOptimization
.isManufacturerBatteryOptimizationDisabled;
if (isManBatteryOptimizationDisabled == false) {
final hasDisabled = await DisableBatteryOptimization
.showDisableManufacturerBatteryOptimizationSettings(
"当前设备可能有额外的电池优化",
"按照步骤操作以禁用电池优化,以保证应用在后台正常运行",
);
// 设置为已禁用
GStrorage.localCache.put(
'isManufacturerBatteryOptimizationDisabled', hasDisabled == true);
}
}
}

View File

@ -1,40 +1,94 @@
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:saver_gallery/saver_gallery.dart';
class DownloadUtils {
// 获取存储权限
static requestStoragePer() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.storage,
Permission.photos,
].request();
statuses[Permission.storage].toString();
static Future<bool> requestStoragePer() async {
await Permission.storage.request();
PermissionStatus status = await Permission.storage.status;
if (status == PermissionStatus.denied ||
status == PermissionStatus.permanentlyDenied) {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('存储权限未授权'),
actions: [
TextButton(
onPressed: () async {
openAppSettings();
},
child: const Text('去授权'),
)
],
);
},
);
return false;
} else {
return true;
}
}
// 获取相册权限
static Future<bool> requestPhotoPer() async {
await Permission.photos.request();
PermissionStatus status = await Permission.photos.status;
if (status == PermissionStatus.denied ||
status == PermissionStatus.permanentlyDenied) {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('相册权限未授权'),
actions: [
TextButton(
onPressed: () async {
openAppSettings();
},
child: const Text('去授权'),
)
],
);
},
);
return false;
} else {
return true;
}
}
static Future<bool> downloadImg(String imgUrl,
{String imgType = 'cover'}) async {
try {
await requestStoragePer();
if (!await requestPhotoPer()) {
return false;
}
SmartDialog.showLoading(msg: '保存中');
var response = await Dio()
.get(imgUrl, options: Options(responseType: ResponseType.bytes));
final String imgSuffix = imgUrl.split('.').last;
String picName =
"plpl_${imgType}_${DateTime.now().toString().split('-').join()}";
"plpl_${imgType}_${DateTime.now().toString().replaceAll(RegExp(r'[- :]'), '').split('.').first}";
final SaveResult result = await SaverGallery.saveImage(
Uint8List.fromList(response.data),
quality: 60,
name: picName,
name: '$picName.$imgSuffix',
// 保存到 PiliPala文件夹
androidRelativePath: "Pictures/PiliPala",
androidExistNotSave: false,
);
SmartDialog.dismiss();
if (result.isSuccess) {
await SmartDialog.showToast('$picName」已保存 ');
await SmartDialog.showToast('${'$picName.$imgSuffix'}」已保存 ');
}
return true;
} catch (err) {

View File

@ -84,6 +84,7 @@ class SettingBoxKey {
autoUpgradeEnable = 'autoUpgradeEnable',
feedBackEnable = 'feedBackEnable',
defaultVideoQa = 'defaultVideoQa',
defaultLiveQa = 'defaultLiveQa',
defaultAudioQa = 'defaultAudioQa',
autoPlayEnable = 'autoPlayEnable',
fullScreenMode = 'fullScreenMode',
@ -171,6 +172,10 @@ class LocalCacheKey {
// 代理host port
systemProxyHost = 'systemProxyHost',
systemProxyPort = 'systemProxyPort';
static const String isDisableBatteryOptLocal = 'isDisableBatteryOptLocal',
isManufacturerBatteryOptimizationDisabled =
'isManufacturerBatteryOptimizationDisabled';
}
class VideoBoxKey {

View File

@ -393,6 +393,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
disable_battery_optimization:
dependency: "direct main"
description:
name: disable_battery_optimization
sha256: "6b2ba802f984af141faf1b6b5fb956d5ef01f9cd555597c35b9cc335a03185ba"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
dismissible_page:
dependency: "direct main"
description:

View File

@ -140,6 +140,8 @@ dependencies:
catcher_2: ^1.1.0
logger: ^2.0.2+1
path: 1.8.3
# 电池优化
disable_battery_optimization: ^1.1.1
dev_dependencies:
flutter_test: