From dd0ccb327b71b7e7316cf31d8ae92c2a930dfaad Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Fri, 21 Nov 2025 18:43:57 +0800 Subject: [PATCH] show battery level Signed-off-by: bggRGjQaUbCoE --- lib/common/widgets/marquee.dart | 4 +- lib/pages/live_room/controller.dart | 3 + lib/pages/live_room/view.dart | 2 + .../live_room/widgets/bottom_control.dart | 2 + .../live_room/widgets/header_control.dart | 52 +++- lib/pages/setting/models/play_settings.dart | 7 + lib/pages/video/controller.dart | 2 +- lib/pages/video/view.dart | 7 +- lib/pages/video/widgets/header_control.dart | 273 +++++++++++------- lib/plugin/pl_player/view.dart | 20 +- lib/utils/storage_key.dart | 3 +- lib/utils/storage_pref.dart | 5 + pubspec.lock | 24 ++ pubspec.yaml | 1 + 14 files changed, 279 insertions(+), 126 deletions(-) diff --git a/lib/common/widgets/marquee.dart b/lib/common/widgets/marquee.dart index e3406f883..be4021070 100644 --- a/lib/common/widgets/marquee.dart +++ b/lib/common/widgets/marquee.dart @@ -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); } } } diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 12c15f06a..73134cbe8 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -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(); + @override void onInit() { super.onInit(); diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index be9c308a3..be03cc917 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -233,11 +233,13 @@ class _LiveRoomPageState extends State 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, diff --git a/lib/pages/live_room/widgets/bottom_control.dart b/lib/pages/live_room/widgets/bottom_control.dart index 879863b97..f510270c0 100644 --- a/lib/pages/live_room/widgets/bottom_control.dart +++ b/lib/pages/live_room/widgets/bottom_control.dart @@ -88,6 +88,7 @@ class _BottomControlState extends State 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 with HeaderMixin { }, ), ComBtn( + height: 30, tooltip: '弹幕设置', icon: const Icon( size: 18, diff --git a/lib/pages/live_room/widgets/header_control.dart b/lib/pages/live_room/widgets/header_control.dart index 8ba67f543..9424814bf 100644 --- a/lib/pages/live_room/widgets/header_control.dart +++ b/lib/pages/live_room/widgets/header_control.dart @@ -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 createState() => _LiveHeaderControlState(); +} + +class _LiveHeaderControlState extends State + 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, diff --git a/lib/pages/setting/models/play_settings.dart b/lib/pages/setting/models/play_settings.dart index e692d476c..263897283 100644 --- a/lib/pages/setting/models/play_settings.dart +++ b/lib/pages/setting/models/play_settings.dart @@ -67,6 +67,13 @@ List 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: '双击快退/快进', diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index c35e3e28e..b8f7726c9 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -134,7 +134,7 @@ class VideoDetailController extends GetxController // 亮度 double? brightness; - late final headerCtrKey = GlobalKey(); + late final headerCtrKey = GlobalKey(); Box setting = GStorage.setting; diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index 49eba350d..8189f5a02 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -767,9 +767,10 @@ class _VideoDetailPageVState extends State ), ), onPressed: () => - videoDetailController - .headerCtrKey - .currentState + (videoDetailController + .headerCtrKey + .currentState + as HeaderControlState?) ?.showSettingSheet(), icon: Icon( Icons.more_vert_outlined, diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 3ab6cfbd4..e771268bd 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -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 on State { + 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? 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 on State { PlPlayerController get plPlayerController; @@ -134,7 +239,7 @@ mixin HeaderMixin on State { final DanmakuController? 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 with HeaderMixin { +class HeaderControlState extends State + with HeaderMixin, TimeBatteryMixin { @override late final PlPlayerController plPlayerController = widget.controller; late final VideoDetailController videoDetailCtr = widget.videoDetailCtr; @@ -837,13 +943,12 @@ class HeaderControlState extends State 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 with HeaderMixin { } } - @override - void dispose() { - cancelClock(); - super.dispose(); - } - /// 设置面板 void showSettingSheet() { showBottomSheet( @@ -2316,26 +2415,6 @@ class HeaderControlState extends State 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 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 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) diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index b6eee0c87..08442da8d 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -156,12 +156,22 @@ class _PLVideoPlayerState extends State _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).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 { diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index faaa277c7..75d5d8fd4 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -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', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index 32bf07b12..002ea60d7 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -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, + ); } diff --git a/pubspec.lock b/pubspec.lock index 08b557a46..4436f9395 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index ceefdc46d..f9b810b68 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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