show battery level

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-11-21 18:43:57 +08:00
parent 919134759b
commit dd0ccb327b
14 changed files with 279 additions and 126 deletions

View File

@@ -288,7 +288,7 @@ class _BounceMarqueeRender extends MarqueeRender {
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
}
} else {
paintCenter(context, offset);
context.paintChild(child!, offset);
}
}
}
@@ -348,7 +348,7 @@ class _NormalMarqueeRender extends MarqueeRender {
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
}
} else {
paintCenter(context, offset);
context.paintChild(child, offset);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: '双击快退/快进',

View File

@@ -134,7 +134,7 @@ class VideoDetailController extends GetxController
// 亮度
double? brightness;
late final headerCtrKey = GlobalKey<HeaderControlState>();
late final headerCtrKey = GlobalKey<TimeBatteryMixin>();
Box setting = GStorage.setting;

View File

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

View File

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

View File

@@ -156,12 +156,22 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
_controlsListener = plPlayerController.showControls.listen((bool val) {
final visible = val && !plPlayerController.controlsLock.value;
if (widget.videoDetailController?.headerCtrKey.currentState?.provider
case final provider?) {
provider
..startIfNeeded()
..muted = !visible;
if ((widget.headerControl.key as GlobalKey<TimeBatteryMixin>).currentState
case final state?) {
if (state.mounted) {
state.getBatteryLevelIfNeeded();
state.provider
?..startIfNeeded()
..muted = !visible;
if (visible) {
state.startClock();
} else {
state.stopClock();
}
}
}
if (visible) {
animationController.forward();
} else {

View File

@@ -28,7 +28,8 @@ abstract class SettingBoxKey {
keyboardControl = 'keyboardControl',
pauseOnMinimize = 'pauseOnMinimize',
pgcSkipType = 'pgcSkipType',
audioPlayMode = 'audioPlayMode';
audioPlayMode = 'audioPlayMode',
showBatteryLevel = 'showBatteryLevel';
static const String enableVerticalExpand = 'enableVerticalExpand',
feedBackEnable = 'feedBackEnable',

View File

@@ -874,4 +874,9 @@ abstract class Pref {
static String? get downloadPath => _setting.get(SettingBoxKey.downloadPath);
static String? get liveCdnUrl => _setting.get(SettingBoxKey.liveCdnUrl);
static bool get showBatteryLevel => _setting.get(
SettingBoxKey.showBatteryLevel,
defaultValue: Utils.isMobile,
);
}

View File

@@ -130,6 +130,22 @@ packages:
url: "https://github.com/bggRGjQaUbCoE/auto_orientation.git"
source: git
version: "2.3.1"
battery_plus:
dependency: "direct main"
description:
name: battery_plus
sha256: ad16fcb55b7384be6b4bbc763d5e2031ac7ea62b2d9b6b661490c7b9741155bf
url: "https://pub.dev"
source: hosted
version: "7.0.0"
battery_plus_platform_interface:
dependency: transitive
description:
name: battery_plus_platform_interface
sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910
url: "https://pub.dev"
source: hosted
version: "2.0.1"
boolean_selector:
dependency: transitive
description:
@@ -1778,6 +1794,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.1"
upower:
dependency: transitive
description:
name: upower
sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf
url: "https://pub.dev"
source: hosted
version: "0.7.0"
uri_parser:
dependency: transitive
description:

View File

@@ -225,6 +225,7 @@ dependencies:
url: https://github.com/bggRGjQaUbCoE/super_sliver_list.git
ref: mod
dlna_dart: ^0.1.0
battery_plus: ^7.0.0
vector_math: any
fixnum: any