mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-05 01:27:49 +08:00
show battery level
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -14,6 +14,7 @@ import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart';
|
||||
import 'package:PiliPlus/models_new/live/live_superchat/item.dart';
|
||||
import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart';
|
||||
import 'package:PiliPlus/pages/live_room/send_danmaku/view.dart';
|
||||
import 'package:PiliPlus/pages/video/widgets/header_control.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/data_source.dart';
|
||||
import 'package:PiliPlus/services/service_locator.dart';
|
||||
@@ -88,6 +89,8 @@ class LiveRoomController extends GetxController {
|
||||
|
||||
final showSuperChat = Pref.showSuperChat;
|
||||
|
||||
final headerKey = GlobalKey<TimeBatteryMixin>();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
@@ -233,11 +233,13 @@ class _LiveRoomPageState extends State<LiveRoomPage>
|
||||
alignment: alignment,
|
||||
plPlayerController: plPlayerController,
|
||||
headerControl: LiveHeaderControl(
|
||||
key: _liveRoomController.headerKey,
|
||||
title: roomInfoH5?.roomInfo?.title,
|
||||
upName: roomInfoH5?.anchorInfo?.baseInfo?.uname,
|
||||
plPlayerController: plPlayerController,
|
||||
onSendDanmaku: _liveRoomController.onSendDanmaku,
|
||||
onPlayAudio: _liveRoomController.queryLiveUrl,
|
||||
isPortrait: isPortrait,
|
||||
),
|
||||
bottomControl: BottomControl(
|
||||
plPlayerController: plPlayerController,
|
||||
|
||||
@@ -88,6 +88,7 @@ class _BottomControlState extends State<BottomControl> with HeaderMixin {
|
||||
final enableShowLiveDanmaku =
|
||||
plPlayerController.enableShowDanmaku.value;
|
||||
return ComBtn(
|
||||
height: 30,
|
||||
tooltip: "${enableShowLiveDanmaku ? '关闭' : '开启'}弹幕",
|
||||
icon: enableShowLiveDanmaku
|
||||
? const Icon(
|
||||
@@ -114,6 +115,7 @@ class _BottomControlState extends State<BottomControl> with HeaderMixin {
|
||||
},
|
||||
),
|
||||
ComBtn(
|
||||
height: 30,
|
||||
tooltip: '弹幕设置',
|
||||
icon: const Icon(
|
||||
size: 18,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/marquee.dart';
|
||||
import 'package:PiliPlus/pages/video/widgets/header_control.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart';
|
||||
@@ -11,7 +12,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
|
||||
class LiveHeaderControl extends StatelessWidget {
|
||||
class LiveHeaderControl extends StatefulWidget {
|
||||
const LiveHeaderControl({
|
||||
super.key,
|
||||
required this.title,
|
||||
@@ -19,6 +20,7 @@ class LiveHeaderControl extends StatelessWidget {
|
||||
required this.plPlayerController,
|
||||
required this.onSendDanmaku,
|
||||
required this.onPlayAudio,
|
||||
required this.isPortrait,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
@@ -26,29 +28,50 @@ class LiveHeaderControl extends StatelessWidget {
|
||||
final PlPlayerController plPlayerController;
|
||||
final VoidCallback onSendDanmaku;
|
||||
final VoidCallback onPlayAudio;
|
||||
final bool isPortrait;
|
||||
|
||||
@override
|
||||
State<LiveHeaderControl> createState() => _LiveHeaderControlState();
|
||||
}
|
||||
|
||||
class _LiveHeaderControlState extends State<LiveHeaderControl>
|
||||
with TimeBatteryMixin {
|
||||
late final plPlayerController = widget.plPlayerController;
|
||||
|
||||
@override
|
||||
bool get horizontalScreen => true;
|
||||
|
||||
@override
|
||||
bool get isFullScreen => plPlayerController.isFullScreen.value;
|
||||
|
||||
@override
|
||||
bool get isPortrait => widget.isPortrait;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isFullScreen = plPlayerController.isFullScreen.value;
|
||||
final isFullScreen = this.isFullScreen;
|
||||
showCurrTimeIfNeeded(isFullScreen);
|
||||
Widget child;
|
||||
if (title != null) {
|
||||
child = Text(
|
||||
title!,
|
||||
maxLines: 1,
|
||||
if (widget.title case final title?) {
|
||||
child = MarqueeText(
|
||||
title,
|
||||
spacing: 30,
|
||||
velocity: 30,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
height: 1,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
if (isFullScreen && upName != null) {
|
||||
if (isFullScreen && widget.upName != null) {
|
||||
child = Column(
|
||||
spacing: 5,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
child,
|
||||
Text(
|
||||
upName!,
|
||||
widget.upName!,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
@@ -70,10 +93,10 @@ class LiveHeaderControl extends StatelessWidget {
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 14,
|
||||
title: Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (isFullScreen || plPlayerController.isDesktopPip)
|
||||
ComBtn(
|
||||
height: 30,
|
||||
tooltip: '返回',
|
||||
icon: const Icon(FontAwesomeIcons.arrowLeft, size: 15),
|
||||
onTap: () {
|
||||
@@ -85,23 +108,27 @@ class LiveHeaderControl extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
child,
|
||||
...?timeBatteryWidgets,
|
||||
const SizedBox(width: 10),
|
||||
ComBtn(
|
||||
height: 30,
|
||||
tooltip: '发弹幕',
|
||||
icon: const Icon(
|
||||
size: 18,
|
||||
Icons.comment_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
onTap: onSendDanmaku,
|
||||
onTap: widget.onSendDanmaku,
|
||||
),
|
||||
Obx(
|
||||
() {
|
||||
final onlyPlayAudio = plPlayerController.onlyPlayAudio.value;
|
||||
return ComBtn(
|
||||
height: 30,
|
||||
tooltip: '仅播放音频',
|
||||
onTap: () {
|
||||
plPlayerController.onlyPlayAudio.value = !onlyPlayAudio;
|
||||
onPlayAudio();
|
||||
widget.onPlayAudio();
|
||||
},
|
||||
icon: onlyPlayAudio
|
||||
? const Icon(
|
||||
@@ -119,6 +146,7 @@ class LiveHeaderControl extends StatelessWidget {
|
||||
),
|
||||
if (Platform.isAndroid || (Utils.isDesktop && !isFullScreen))
|
||||
ComBtn(
|
||||
height: 30,
|
||||
tooltip: '画中画',
|
||||
onTap: () async {
|
||||
if (Utils.isDesktop) {
|
||||
@@ -138,6 +166,7 @@ class LiveHeaderControl extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
ComBtn(
|
||||
height: 30,
|
||||
tooltip: '定时关闭',
|
||||
onTap: () => PageUtils.scheduleExit(context, isFullScreen, true),
|
||||
icon: const Icon(
|
||||
@@ -147,6 +176,7 @@ class LiveHeaderControl extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
ComBtn(
|
||||
height: 30,
|
||||
tooltip: '播放信息',
|
||||
onTap: () => HeaderControlState.showPlayerInfo(
|
||||
context,
|
||||
|
||||
@@ -67,6 +67,13 @@ List<SettingsModel> get playSettings => [
|
||||
setKey: SettingBoxKey.showFsScreenshotBtn,
|
||||
defaultVal: true,
|
||||
),
|
||||
SettingsModel(
|
||||
settingsType: SettingsType.sw1tch,
|
||||
title: '全屏显示电池电量',
|
||||
leading: const Icon(Icons.battery_3_bar),
|
||||
setKey: SettingBoxKey.showBatteryLevel,
|
||||
defaultVal: Utils.isMobile,
|
||||
),
|
||||
const SettingsModel(
|
||||
settingsType: SettingsType.sw1tch,
|
||||
title: '双击快退/快进',
|
||||
|
||||
@@ -134,7 +134,7 @@ class VideoDetailController extends GetxController
|
||||
// 亮度
|
||||
double? brightness;
|
||||
|
||||
late final headerCtrKey = GlobalKey<HeaderControlState>();
|
||||
late final headerCtrKey = GlobalKey<TimeBatteryMixin>();
|
||||
|
||||
Box setting = GStorage.setting;
|
||||
|
||||
|
||||
@@ -767,9 +767,10 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
),
|
||||
),
|
||||
onPressed: () =>
|
||||
videoDetailController
|
||||
.headerCtrKey
|
||||
.currentState
|
||||
(videoDetailController
|
||||
.headerCtrKey
|
||||
.currentState
|
||||
as HeaderControlState?)
|
||||
?.showSettingSheet(),
|
||||
icon: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
|
||||
@@ -42,10 +42,13 @@ import 'package:PiliPlus/utils/image_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_key.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/video_utils.dart';
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
import 'package:canvas_danmaku/canvas_danmaku.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -57,6 +60,108 @@ import 'package:hive/hive.dart';
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
|
||||
mixin TimeBatteryMixin<T extends StatefulWidget> on State<T> {
|
||||
ContextSingleTicker? provider;
|
||||
ContextSingleTicker get effectiveProvider =>
|
||||
provider ??= ContextSingleTicker(context, autoStart: false);
|
||||
|
||||
bool get isPortrait;
|
||||
bool get isFullScreen;
|
||||
bool get horizontalScreen;
|
||||
|
||||
Timer? _clock;
|
||||
RxString now = ''.obs;
|
||||
|
||||
static final _format = DateFormat('HH:mm');
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stopClock();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void startClock() {
|
||||
if (!_showCurrTime) return;
|
||||
if (_clock == null) {
|
||||
now.value = _format.format(DateTime.now());
|
||||
_clock ??= Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||
if (!mounted) {
|
||||
stopClock();
|
||||
return;
|
||||
}
|
||||
now.value = _format.format(DateTime.now());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void stopClock() {
|
||||
_clock?.cancel();
|
||||
_clock = null;
|
||||
}
|
||||
|
||||
bool _showCurrTime = false;
|
||||
void showCurrTimeIfNeeded(bool isFullScreen) {
|
||||
_showCurrTime = !isPortrait && (isFullScreen || !horizontalScreen);
|
||||
if (_showCurrTime) {
|
||||
now.value = _format.format(DateTime.now());
|
||||
getBatteryLevelIfNeeded();
|
||||
} else {
|
||||
stopClock();
|
||||
}
|
||||
}
|
||||
|
||||
late final _battery = Battery();
|
||||
late final RxnInt _batteryLevel = RxnInt();
|
||||
late final _showBatteryLevel = Pref.showBatteryLevel;
|
||||
void getBatteryLevelIfNeeded() {
|
||||
if (!_showCurrTime || !_showBatteryLevel) return;
|
||||
EasyThrottle.throttle(
|
||||
'getBatteryLevel$hashCode',
|
||||
const Duration(seconds: 30),
|
||||
() async {
|
||||
try {
|
||||
_batteryLevel.value = await _battery.batteryLevel;
|
||||
} catch (_) {}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget>? get timeBatteryWidgets {
|
||||
if (_showCurrTime) {
|
||||
return [
|
||||
if (_showBatteryLevel) ...[
|
||||
Obx(
|
||||
() {
|
||||
final batteryLevel = _batteryLevel.value;
|
||||
if (batteryLevel == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Text(
|
||||
'$batteryLevel%',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
Obx(
|
||||
() => Text(
|
||||
now.value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
mixin HeaderMixin<T extends StatefulWidget> on State<T> {
|
||||
PlPlayerController get plPlayerController;
|
||||
|
||||
@@ -134,7 +239,7 @@ mixin HeaderMixin<T extends StatefulWidget> on State<T> {
|
||||
final DanmakuController<DanmakuExtra>? danmakuController =
|
||||
plPlayerController.danmakuController;
|
||||
|
||||
final isFullScreen = plPlayerController.isFullScreen.value;
|
||||
final isFullScreen = this.isFullScreen;
|
||||
|
||||
showBottomSheet(
|
||||
(context, setState) {
|
||||
@@ -819,7 +924,8 @@ class HeaderControl extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderControlState extends State<HeaderControl> with HeaderMixin {
|
||||
class HeaderControlState extends State<HeaderControl>
|
||||
with HeaderMixin, TimeBatteryMixin {
|
||||
@override
|
||||
late final PlPlayerController plPlayerController = widget.controller;
|
||||
late final VideoDetailController videoDetailCtr = widget.videoDetailCtr;
|
||||
@@ -837,13 +943,12 @@ class HeaderControlState extends State<HeaderControl> with HeaderMixin {
|
||||
? ugcIntroController
|
||||
: pgcIntroController;
|
||||
|
||||
@override
|
||||
bool get isPortrait => widget.isPortrait;
|
||||
@override
|
||||
late final horizontalScreen = videoDetailCtr.horizontalScreen;
|
||||
RxString now = ''.obs;
|
||||
Timer? clock;
|
||||
|
||||
Box setting = GStorage.setting;
|
||||
late final provider = ContextSingleTicker(context, autoStart: false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -857,12 +962,6 @@ class HeaderControlState extends State<HeaderControl> with HeaderMixin {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cancelClock();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 设置面板
|
||||
void showSettingSheet() {
|
||||
showBottomSheet(
|
||||
@@ -2316,26 +2415,6 @@ class HeaderControlState extends State<HeaderControl> with HeaderMixin {
|
||||
);
|
||||
}
|
||||
|
||||
static final _format = DateFormat('HH:mm');
|
||||
|
||||
void startClock() {
|
||||
if (clock == null) {
|
||||
now.value = _format.format(DateTime.now());
|
||||
clock = Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||
if (!mounted) {
|
||||
cancelClock();
|
||||
return;
|
||||
}
|
||||
now.value = _format.format(DateTime.now());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void cancelClock() {
|
||||
clock?.cancel();
|
||||
clock = null;
|
||||
}
|
||||
|
||||
late final isFileSource = videoDetailCtr.isFileSource;
|
||||
|
||||
@override
|
||||
@@ -2344,11 +2423,65 @@ class HeaderControlState extends State<HeaderControl> with HeaderMixin {
|
||||
final isFSOrPip = isFullScreen || plPlayerController.isDesktopPip;
|
||||
final showFSActionItem =
|
||||
!isFileSource && plPlayerController.showFSActionItem && isFSOrPip;
|
||||
final showCurrTime = !isPortrait && (isFullScreen || !horizontalScreen);
|
||||
if (showCurrTime) {
|
||||
startClock();
|
||||
showCurrTimeIfNeeded(isFullScreen);
|
||||
Widget title;
|
||||
if (introController.videoDetail.value.title != null &&
|
||||
(isFullScreen ||
|
||||
((!horizontalScreen || plPlayerController.isDesktopPip) &&
|
||||
!isPortrait))) {
|
||||
title = Padding(
|
||||
padding: isPortrait
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.only(right: 10),
|
||||
child: Obx(
|
||||
() {
|
||||
final videoDetail = introController.videoDetail.value;
|
||||
final String title;
|
||||
if (isFileSource || videoDetail.videos == 1) {
|
||||
title = videoDetail.title!;
|
||||
} else {
|
||||
title =
|
||||
videoDetail.pages
|
||||
?.firstWhereOrNull(
|
||||
(e) => e.cid == videoDetailCtr.cid.value,
|
||||
)
|
||||
?.part ??
|
||||
videoDetail.title!;
|
||||
}
|
||||
return MarqueeText(
|
||||
title,
|
||||
spacing: 30,
|
||||
velocity: 30,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
provider: effectiveProvider,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (introController.isShowOnlineTotal) {
|
||||
title = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title,
|
||||
Obx(
|
||||
() => Text(
|
||||
'${introController.total.value}人正在看',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
title = Expanded(child: title);
|
||||
} else {
|
||||
cancelClock();
|
||||
title = const Spacer();
|
||||
}
|
||||
return AppBar(
|
||||
elevation: 0,
|
||||
@@ -2409,75 +2542,9 @@ class HeaderControlState extends State<HeaderControl> with HeaderMixin {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (introController.videoDetail.value.title != null &&
|
||||
(isFullScreen ||
|
||||
((!horizontalScreen || plPlayerController.isDesktopPip) &&
|
||||
!isPortrait)))
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: isPortrait
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.only(right: 10),
|
||||
child: Obx(
|
||||
() {
|
||||
final videoDetail =
|
||||
introController.videoDetail.value;
|
||||
final String title;
|
||||
if (isFileSource || videoDetail.videos == 1) {
|
||||
title = videoDetail.title!;
|
||||
} else {
|
||||
title =
|
||||
videoDetail.pages
|
||||
?.firstWhereOrNull(
|
||||
(e) =>
|
||||
e.cid == videoDetailCtr.cid.value,
|
||||
)
|
||||
?.part ??
|
||||
videoDetail.title!;
|
||||
}
|
||||
return MarqueeText(
|
||||
title,
|
||||
spacing: 30,
|
||||
velocity: 30,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
provider: provider,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (introController.isShowOnlineTotal)
|
||||
Obx(
|
||||
() => Text(
|
||||
'${introController.total.value}人正在看',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
const Spacer(),
|
||||
title,
|
||||
// show current datetime
|
||||
if (showCurrTime)
|
||||
Obx(
|
||||
() => Text(
|
||||
now.value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
...?timeBatteryWidgets,
|
||||
if (!isFileSource) ...[
|
||||
if (!isFSOrPip) ...[
|
||||
if (videoDetailCtr.isUgc)
|
||||
|
||||
Reference in New Issue
Block a user