From 86abf006d06a5a027a9cee344dcfaf7c844fff74 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Wed, 2 Apr 2025 18:03:47 +0800 Subject: [PATCH] mod: live schedule Closes #581 Signed-off-by: bggRGjQaUbCoE --- lib/pages/live_room/controller.dart | 38 ++-- lib/pages/live_room/view.dart | 19 +- .../live_room/widgets/bottom_control.dart | 50 +---- .../live_room/widgets/header_control.dart | 89 ++++++++ .../video/detail/widgets/header_control.dart | 188 +---------------- .../pl_player/widgets/play_pause_btn.dart | 2 +- lib/tcp/live.dart | 93 +++++---- lib/utils/utils.dart | 192 ++++++++++++++++++ 8 files changed, 378 insertions(+), 293 deletions(-) create mode 100644 lib/pages/live_room/widgets/header_control.dart diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index dbf88ec34..4e0f95fd9 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -145,24 +145,26 @@ class LiveRoomController extends GetxController { } void liveMsg() { - LiveHttp.liveRoomDanmaPrefetch(roomId: roomId).then((v) { - if (v['status']) { - messages.addAll((v['data'] as List) - .map((obj) => { - 'name': obj['user']['base']['name'], - 'uid': obj['user']['uid'], - 'text': obj['text'], - 'emots': obj['emots'], - 'uemote': obj['emoticon']['emoticon_unique'] != "" - ? obj['emoticon'] - : null, - }) - .toList()); - WidgetsBinding.instance.addPostFrameCallback( - (_) => scrollToBottom(), - ); - } - }); + if (messages.isEmpty) { + LiveHttp.liveRoomDanmaPrefetch(roomId: roomId).then((v) { + if (v['status']) { + messages.addAll((v['data'] as List) + .map((obj) => { + 'name': obj['user']['base']['name'], + 'uid': obj['user']['uid'], + 'text': obj['text'], + 'emots': obj['emots'], + 'uemote': obj['emoticon']['emoticon_unique'] != "" + ? obj['emoticon'] + : null, + }) + .toList()); + WidgetsBinding.instance.addPostFrameCallback( + (_) => scrollToBottom(), + ); + } + }); + } LiveHttp.liveRoomGetDanmakuToken(roomId: roomId).then((v) { if (v['status']) { LiveDanmakuInfo info = v['data']; diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 97d2a545d..a65f218ba 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:PiliPlus/http/live.dart'; import 'package:PiliPlus/pages/live_room/widgets/chat.dart'; +import 'package:PiliPlus/pages/live_room/widgets/header_control.dart'; import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -75,7 +76,7 @@ class _LiveRoomPageState extends State videoSourceInit(); _futureBuilderFuture = _liveRoomController.queryLiveInfo(); plPlayerController.autoEnterFullscreen(); - _liveRoomController.liveMsg(); + plPlayerController.addStatusLister(playerListener); _listener = plPlayerController.isFullScreen.listen((isFullScreen) { if (isFullScreen != _isFullScreen) { _isFullScreen = isFullScreen; @@ -84,6 +85,16 @@ class _LiveRoomPageState extends State }); } + void playerListener(PlayerStatus? status) { + if (status != PlayerStatus.playing) { + plPlayerController.danmakuController?.pause(); + _liveRoomController.msgStream?.close(); + } else { + plPlayerController.danmakuController?.resume(); + _liveRoomController.liveMsg(); + } + } + void _updateFontSize() async { if (Platform.isAndroid) { _isPipMode = @@ -119,6 +130,7 @@ class _LiveRoomPageState extends State _liveRoomController.msgStream?.close(); // floating?.dispose(); _node.dispose(); + plPlayerController.removeStatusLister(playerListener); plPlayerController.dispose(); _ctr.dispose(); super.dispose(); @@ -160,10 +172,13 @@ class _LiveRoomPageState extends State fill: fill, alignment: alignment, plPlayerController: plPlayerController, + headerControl: LiveHeaderControl( + plPlayerController: plPlayerController, + floating: floating, + ), bottomControl: BottomControl( plPlayerController: plPlayerController, liveRoomCtr: _liveRoomController, - floating: floating, onRefresh: () { _futureBuilderFuture = _liveRoomController.queryLiveInfo(); }, diff --git a/lib/pages/live_room/widgets/bottom_control.dart b/lib/pages/live_room/widgets/bottom_control.dart index 81463f34a..0c6626b24 100644 --- a/lib/pages/live_room/widgets/bottom_control.dart +++ b/lib/pages/live_room/widgets/bottom_control.dart @@ -1,25 +1,20 @@ -import 'dart:io'; - +import 'package:PiliPlus/plugin/pl_player/widgets/play_pause_btn.dart'; import 'package:PiliPlus/utils/storage.dart'; -import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; import 'package:PiliPlus/pages/live_room/index.dart'; import 'package:PiliPlus/plugin/pl_player/index.dart'; import 'package:get/get.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class BottomControl extends StatelessWidget implements PreferredSizeWidget { const BottomControl({ required this.plPlayerController, required this.liveRoomCtr, - this.floating, required this.onRefresh, super.key, }); final PlPlayerController plPlayerController; final LiveRoomController liveRoomCtr; - final Floating? floating; final VoidCallback onRefresh; final TextStyle subTitleStyle = const TextStyle(fontSize: 12); @@ -39,6 +34,10 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { titleSpacing: 14, title: Row( children: [ + PlayOrPauseButton( + plPlayerController: plPlayerController, + ), + const SizedBox(width: 10), ComBtn( icon: const Icon( Icons.refresh, @@ -78,18 +77,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { // ), // ), // const SizedBox(width: 4), - Obx( - () => IconButton( - onPressed: plPlayerController.setOnlyPlayAudio, - icon: Icon( - size: 18, - plPlayerController.onlyPlayAudio.value - ? MdiIcons.musicCircle - : MdiIcons.musicCircleOutline, - color: Colors.white, - ), - ), - ), + Obx( () => IconButton( onPressed: () { @@ -140,32 +128,6 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { ), ), const SizedBox(width: 10), - if (Platform.isAndroid) ...[ - SizedBox( - width: 34, - height: 34, - child: IconButton( - tooltip: '画中画', - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero), - ), - onPressed: () async { - try { - if ((await floating?.isPipAvailable) == true) { - plPlayerController.hiddenControls(false); - floating!.enable(const EnableManual()); - } - } catch (_) {} - }, - icon: const Icon( - Icons.picture_in_picture_outlined, - size: 18, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 10), - ], Obx( () => SizedBox( width: 30, diff --git a/lib/pages/live_room/widgets/header_control.dart b/lib/pages/live_room/widgets/header_control.dart new file mode 100644 index 000000000..27f69226c --- /dev/null +++ b/lib/pages/live_room/widgets/header_control.dart @@ -0,0 +1,89 @@ +import 'dart:io'; + +import 'package:PiliPlus/utils/utils.dart'; +import 'package:floating/floating.dart'; +import 'package:flutter/material.dart'; +import 'package:PiliPlus/plugin/pl_player/index.dart'; +import 'package:get/get.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class LiveHeaderControl extends StatelessWidget implements PreferredSizeWidget { + const LiveHeaderControl({ + required this.plPlayerController, + this.floating, + super.key, + }); + + final Floating? floating; + final PlPlayerController plPlayerController; + + @override + Size get preferredSize => const Size(double.infinity, kToolbarHeight); + + @override + Widget build(BuildContext context) { + return AppBar( + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + primary: false, + automaticallyImplyLeading: false, + titleSpacing: 14, + title: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Obx( + () => IconButton( + onPressed: plPlayerController.setOnlyPlayAudio, + icon: Icon( + size: 18, + plPlayerController.onlyPlayAudio.value + ? MdiIcons.musicCircle + : MdiIcons.musicCircleOutline, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 10), + if (Platform.isAndroid) ...[ + SizedBox( + width: 34, + height: 34, + child: IconButton( + tooltip: '画中画', + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + ), + onPressed: () async { + try { + if ((await floating?.isPipAvailable) == true) { + plPlayerController.hiddenControls(false); + floating!.enable(const EnableManual()); + } + } catch (_) {} + }, + icon: const Icon( + Icons.picture_in_picture_outlined, + size: 18, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 10), + ], + IconButton( + onPressed: () => Utils.scheduleExit( + context, + plPlayerController.isFullScreen.value, + true, + ), + icon: Icon( + size: 18, + Icons.schedule, + color: Colors.white, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 08a6a0f73..0ec21d336 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -33,7 +33,6 @@ import 'package:PiliPlus/pages/video/detail/introduction/widgets/menu_row.dart'; import 'package:PiliPlus/plugin/pl_player/index.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/utils/storage.dart'; -import 'package:PiliPlus/services/shutdown_timer_service.dart'; import 'package:share_plus/share_plus.dart'; import '../../../../models/video/play/CDN.dart'; import '../../../setting/widgets/select_dialog.dart'; @@ -179,7 +178,8 @@ class HeaderControlState extends State { ), ListTile( dense: true, - onTap: () => {Get.back(), scheduleExit()}, + onTap: () => + {Get.back(), Utils.scheduleExit(context, isFullScreen)}, leading: const Icon(Icons.hourglass_top_outlined, size: 20), title: const Text('定时关闭', style: titleStyle), ), @@ -592,190 +592,6 @@ class HeaderControlState extends State { ); } - /// 定时关闭 - void scheduleExit() { - const List scheduleTimeChoices = [0, 15, 30, 45, 60]; - Utils.showFSSheet( - context, - isFullScreen: () => isFullScreen, - child: StatefulBuilder( - builder: (_, setState) { - return Theme( - data: Theme.of(context), - child: Material( - color: Colors.transparent, - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.all(Radius.circular(12)), - ), - margin: const EdgeInsets.all(12), - padding: const EdgeInsets.only(left: 14, right: 14), - child: ListView( - padding: - const EdgeInsets.symmetric(vertical: 0, horizontal: 20), - children: [ - const SizedBox(height: 10), - const Center(child: Text('定时关闭', style: titleStyle)), - const SizedBox(height: 10), - ...[ - ...[ - ...scheduleTimeChoices, - if (scheduleTimeChoices - .contains( - shutdownTimerService.scheduledExitInMinutes) - .not) - shutdownTimerService.scheduledExitInMinutes, - ]..sort(), - -1, - ].map( - (choice) => ListTile( - dense: true, - onTap: () { - if (choice == -1) { - showDialog( - context: context, - builder: (context) { - String duration = ''; - return AlertDialog( - title: const Text('自定义时长'), - content: TextField( - autofocus: true, - onChanged: (value) => duration = value, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp(r'\d+')), - ], - decoration: const InputDecoration( - suffixText: 'min'), - ), - actions: [ - TextButton( - onPressed: Get.back, - child: Text( - '取消', - style: TextStyle( - color: Theme.of(context) - .colorScheme - .outline), - ), - ), - TextButton( - onPressed: () { - Get.back(); - int choice = - int.tryParse(duration) ?? 0; - shutdownTimerService - .scheduledExitInMinutes = choice; - shutdownTimerService - .startShutdownTimer(); - setState(() {}); - }, - child: Text('确定'), - ), - ], - ); - }, - ); - } else { - Get.back(); - shutdownTimerService.scheduledExitInMinutes = - choice; - shutdownTimerService.startShutdownTimer(); - } - }, - contentPadding: const EdgeInsets.only(), - title: Text(choice == -1 - ? '自定义' - : choice == 0 - ? "禁用" - : "$choice分钟后"), - trailing: shutdownTimerService.scheduledExitInMinutes == - choice - ? Icon( - Icons.done, - color: Theme.of(context).colorScheme.primary, - ) - : null, - ), - ), - const SizedBox(height: 6), - const Center( - child: SizedBox( - width: 125, - child: Divider(height: 1), - ), - ), - const SizedBox(height: 10), - ListTile( - dense: true, - onTap: () { - shutdownTimerService.waitForPlayingCompleted = - !shutdownTimerService.waitForPlayingCompleted; - setState(() {}); - }, - contentPadding: const EdgeInsets.only(), - title: const Text("额外等待视频播放完毕", style: titleStyle), - trailing: Transform.scale( - alignment: Alignment - .centerRight, // 缩放Switch的大小后保持右侧对齐, 避免右侧空隙过大 - scale: 0.8, - child: Switch( - thumbIcon: WidgetStateProperty.resolveWith( - (Set states) { - if (states.isNotEmpty && - states.first == WidgetState.selected) { - return const Icon(Icons.done); - } - return null; - }), - value: shutdownTimerService.waitForPlayingCompleted, - onChanged: (value) => setState(() => - shutdownTimerService.waitForPlayingCompleted = - value), - ), - ), - ), - const SizedBox(height: 10), - Row( - children: [ - const Text('倒计时结束:', style: titleStyle), - const Spacer(), - ActionRowLineItem( - onTap: () { - shutdownTimerService.exitApp = false; - setState(() {}); - // Get.back(); - }, - text: " 暂停视频 ", - selectStatus: !shutdownTimerService.exitApp, - ), - const Spacer(), - // const SizedBox(width: 10), - ActionRowLineItem( - onTap: () { - shutdownTimerService.exitApp = true; - setState(() {}); - // Get.back(); - }, - text: " 退出APP ", - selectStatus: shutdownTimerService.exitApp, - ) - ], - ), - const SizedBox(height: 10), - ], - ), - ), - ), - ); - }, - ), - ); - } - /// 选择画质 void showSetVideoQa() { if (videoInfo.dash == null) { diff --git a/lib/plugin/pl_player/widgets/play_pause_btn.dart b/lib/plugin/pl_player/widgets/play_pause_btn.dart index c49c1d33e..3ec37c1f5 100644 --- a/lib/plugin/pl_player/widgets/play_pause_btn.dart +++ b/lib/plugin/pl_player/widgets/play_pause_btn.dart @@ -68,7 +68,7 @@ class PlayOrPauseButtonState extends State return SizedBox( width: 42, height: 34, - child: InkWell( + child: GestureDetector( onTap: () async { if (player.state.completed) { await player.seek(Duration.zero); diff --git a/lib/tcp/live.dart b/lib/tcp/live.dart index 4263f2599..a6c5b06dc 100644 --- a/lib/tcp/live.dart +++ b/lib/tcp/live.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -156,12 +157,15 @@ class LiveMessageStream { int roomId, uid; List servers; List eventListeners = []; - LiveMessageStream( - {required this.streamToken, - required this.roomId, - required this.uid, - required this.servers}); + LiveMessageStream({ + required this.streamToken, + required this.roomId, + required this.uid, + required this.servers, + }); + WebSocket? socket; + StreamSubscription? _socketSubscription; bool heartBeat = true; PiliLogger logger = getLogger(); final String logTag = "LiveStreamService"; @@ -202,38 +206,39 @@ class LiveMessageStream { socket = await getSocket(); // logger.d('$logTag ===> TCP连接建立'); - socket?.add(authPackage.marshal()); // logger.d('$logTag ===> 发送认证包'); - await for (var data in socket!) { - PackageHeader? header = PackageHeader.fromBytesData(data); - if (header != null) { - List decompressedData = []; - //心跳包回复不用处理 - if (header.operationCode == 3) continue; - if (header.operationCode == 8) { - _heartBeat(); - } - try { - switch (header.protocolVer) { - case 0: - case 1: - _processingData(data); - continue; - case 2: - decompressedData = ZLibDecoder().convert(data.sublist(0x10)); - break; - case 3: - decompressedData = - const BrotliDecoder().convert(data.sublist(0x10)); - //debugPrint('Body: ${utf8.decode()}'); + _socketSubscription = socket?.listen( + (data) { + PackageHeader? header = PackageHeader.fromBytesData(data); + if (header != null) { + List decompressedData = []; + //心跳包回复不用处理 + if (header.operationCode == 3) return; + if (header.operationCode == 8) { + _heartBeat(); + } + try { + switch (header.protocolVer) { + case 0: + case 1: + _processingData(data); + return; + case 2: + decompressedData = ZLibDecoder().convert(data.sublist(0x10)); + break; + case 3: + decompressedData = + const BrotliDecoder().convert(data.sublist(0x10)); + //debugPrint('Body: ${utf8.decode()}'); + } + _processingData(decompressedData); + } catch (e) { + logger.i(e); } - _processingData(decompressedData); - } catch (e) { - logger.i(e); } - } - } - socket?.close(); + }, + ); + socket?.add(authPackage.marshal()); } catch (e) { SmartDialog.showToast("弹幕地址链接失败"); // logger.i('$logTag ===> TCP连接失败: $e'); @@ -266,12 +271,14 @@ class LiveMessageStream { await Future.delayed(const Duration(seconds: 30)); //发送心跳包 var package = HeartbeatPackage( - header: PackageHeader( - totalSize: 0, - headerSize: 0, - protocolVer: 1, - operationCode: 2, - seq: heartBeatCount)); + header: PackageHeader( + totalSize: 0, + headerSize: 0, + protocolVer: 1, + operationCode: 2, + seq: heartBeatCount, + ), + ); try { socket?.add(package.marshal()); } catch (_) {} @@ -283,8 +290,10 @@ class LiveMessageStream { eventListeners.add(func); } - void close() { - socket?.close(); + void close() async { heartBeat = false; + eventListeners.clear(); + _socketSubscription?.cancel(); + socket?.close(); } } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 7828abac7..5c0254ad9 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -26,6 +26,8 @@ import 'package:PiliPlus/pages/dynamics/tab/controller.dart'; import 'package:PiliPlus/pages/later/controller.dart'; import 'package:PiliPlus/pages/video/detail/introduction/widgets/fav_panel.dart'; import 'package:PiliPlus/pages/video/detail/introduction/widgets/group_panel.dart'; +import 'package:PiliPlus/pages/video/detail/introduction/widgets/menu_row.dart'; +import 'package:PiliPlus/services/shutdown_timer_service.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension.dart'; @@ -60,6 +62,196 @@ class Utils { static final _numRegExp = RegExp(r'([\d\.]+)([千万亿])?'); + /// 定时关闭 + static void scheduleExit(context, isFullScreen, [bool isLive = false]) { + const List scheduleTimeChoices = [0, 15, 30, 45, 60]; + const TextStyle titleStyle = TextStyle(fontSize: 14); + if (isLive) { + shutdownTimerService.waitForPlayingCompleted = false; + } + Utils.showFSSheet( + context, + isFullScreen: () => isFullScreen, + child: StatefulBuilder( + builder: (_, setState) { + return Theme( + data: Theme.of(context), + child: Material( + color: Colors.transparent, + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(12), + padding: const EdgeInsets.only(left: 14, right: 14), + child: ListView( + padding: + const EdgeInsets.symmetric(vertical: 0, horizontal: 20), + children: [ + const SizedBox(height: 10), + const Center(child: Text('定时关闭', style: titleStyle)), + const SizedBox(height: 10), + ...[ + ...[ + ...scheduleTimeChoices, + if (scheduleTimeChoices + .contains( + shutdownTimerService.scheduledExitInMinutes) + .not) + shutdownTimerService.scheduledExitInMinutes, + ]..sort(), + -1, + ].map( + (choice) => ListTile( + dense: true, + onTap: () { + if (choice == -1) { + showDialog( + context: context, + builder: (context) { + String duration = ''; + return AlertDialog( + title: const Text('自定义时长'), + content: TextField( + autofocus: true, + onChanged: (value) => duration = value, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'\d+')), + ], + decoration: const InputDecoration( + suffixText: 'min'), + ), + actions: [ + TextButton( + onPressed: Get.back, + child: Text( + '取消', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .outline), + ), + ), + TextButton( + onPressed: () { + Get.back(); + int choice = + int.tryParse(duration) ?? 0; + shutdownTimerService + .scheduledExitInMinutes = choice; + shutdownTimerService + .startShutdownTimer(); + setState(() {}); + }, + child: Text('确定'), + ), + ], + ); + }, + ); + } else { + Get.back(); + shutdownTimerService.scheduledExitInMinutes = + choice; + shutdownTimerService.startShutdownTimer(); + } + }, + contentPadding: const EdgeInsets.only(), + title: Text(choice == -1 + ? '自定义' + : choice == 0 + ? "禁用" + : "$choice分钟后"), + trailing: shutdownTimerService.scheduledExitInMinutes == + choice + ? Icon( + Icons.done, + color: Theme.of(context).colorScheme.primary, + ) + : null, + ), + ), + const SizedBox(height: 6), + const Center( + child: SizedBox( + width: 125, + child: Divider(height: 1), + ), + ), + if (isLive.not) ...[ + const SizedBox(height: 10), + ListTile( + dense: true, + onTap: () { + shutdownTimerService.waitForPlayingCompleted = + !shutdownTimerService.waitForPlayingCompleted; + setState(() {}); + }, + contentPadding: const EdgeInsets.only(), + title: const Text("额外等待视频播放完毕", style: titleStyle), + trailing: Transform.scale( + alignment: Alignment + .centerRight, // 缩放Switch的大小后保持右侧对齐, 避免右侧空隙过大 + scale: 0.8, + child: Switch( + thumbIcon: WidgetStateProperty.resolveWith( + (Set states) { + if (states.isNotEmpty && + states.first == WidgetState.selected) { + return const Icon(Icons.done); + } + return null; + }), + value: shutdownTimerService.waitForPlayingCompleted, + onChanged: (value) => setState(() => + shutdownTimerService.waitForPlayingCompleted = + value), + ), + ), + ), + ], + const SizedBox(height: 10), + Row( + children: [ + const Text('倒计时结束:', style: titleStyle), + const Spacer(), + ActionRowLineItem( + onTap: () { + shutdownTimerService.exitApp = false; + setState(() {}); + // Get.back(); + }, + text: " 暂停视频 ", + selectStatus: !shutdownTimerService.exitApp, + ), + const Spacer(), + // const SizedBox(width: 10), + ActionRowLineItem( + onTap: () { + shutdownTimerService.exitApp = true; + setState(() {}); + // Get.back(); + }, + text: " 退出APP ", + selectStatus: shutdownTimerService.exitApp, + ) + ], + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ), + ); + } + static void enterPip(Floating floating, int width, int height) { Rational aspectRatio = Rational(width, height); floating.enable(