diff --git a/lib/common/widgets/flutter/time_picker.dart b/lib/common/widgets/flutter/time_picker.dart deleted file mode 100644 index a72bdaacc..000000000 --- a/lib/common/widgets/flutter/time_picker.dart +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -Future showTimePicker({ - required BuildContext context, - required TimeOfDay initialTime, - TransitionBuilder? builder, - bool barrierDismissible = true, - Color? barrierColor, - String? barrierLabel, - bool useRootNavigator = true, - TimePickerEntryMode initialEntryMode = TimePickerEntryMode.dial, - String? cancelText, - String? confirmText, - String? helpText, - String? errorInvalidText, - String? hourLabelText, - String? minuteLabelText, - RouteSettings? routeSettings, - EntryModeChangeCallback? onEntryModeChanged, - Offset? anchorPoint, - Orientation? orientation, - Icon? switchToInputEntryModeIcon, - Icon? switchToTimerEntryModeIcon, - bool emptyInitialInput = false, - BoxConstraints? constraints, -}) { - assert(debugCheckHasMaterialLocalizations(context)); - - final Widget dialog = DialogTheme( - data: DialogTheme.of( - context, - ).copyWith(constraints: const BoxConstraints(minWidth: 280.0)), - child: TimePickerDialog( - initialTime: initialTime, - initialEntryMode: initialEntryMode, - cancelText: cancelText, - confirmText: confirmText, - helpText: helpText, - errorInvalidText: errorInvalidText, - hourLabelText: hourLabelText, - minuteLabelText: minuteLabelText, - orientation: orientation, - onEntryModeChanged: onEntryModeChanged, - switchToInputEntryModeIcon: switchToInputEntryModeIcon, - switchToTimerEntryModeIcon: switchToTimerEntryModeIcon, - emptyInitialInput: emptyInitialInput, - ), - ); - return showDialog( - context: context, - barrierDismissible: barrierDismissible, - barrierColor: barrierColor, - barrierLabel: barrierLabel, - useRootNavigator: useRootNavigator, - builder: (BuildContext context) { - return builder == null ? dialog : builder(context, dialog); - }, - routeSettings: routeSettings, - anchorPoint: anchorPoint, - ); -} diff --git a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart index 67bccec4c..54ba24a10 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart @@ -144,7 +144,7 @@ class _InteractiveviewerGalleryState extends State ..removeListener(listener) ..dispose(); _transformationController.dispose(); - _horizontalDragGestureRecognizer.dispose(); + // _horizontalDragGestureRecognizer.dispose(); // duplicate if (widget.quality != _quality) { for (final item in widget.sources) { if (item.sourceType == SourceType.networkImage) { diff --git a/lib/common/widgets/time_picker.dart b/lib/common/widgets/time_picker.dart new file mode 100644 index 000000000..16818aadc --- /dev/null +++ b/lib/common/widgets/time_picker.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart' as material; + +Future showTimePicker({ + required material.BuildContext context, + required material.TimeOfDay initialTime, +}) => material.showTimePicker( + context: context, + initialTime: initialTime, + builder: (context, child) => material.DialogTheme( + data: material.DialogTheme.of( + context, + ).copyWith(constraints: const material.BoxConstraints(minWidth: 280)), + child: child, + ), +); diff --git a/lib/pages/audio/controller.dart b/lib/pages/audio/controller.dart index c41907e00..df0c01727 100644 --- a/lib/pages/audio/controller.dart +++ b/lib/pages/audio/controller.dart @@ -25,6 +25,7 @@ import 'package:PiliPlus/pages/video/pay_coins/view.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/services/service_locator.dart'; +import 'package:PiliPlus/services/shutdown_timer_service.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; @@ -131,16 +132,16 @@ class AudioController extends GetxController ); } - Future onPlay() async { - await player?.play(); + Future? onPlay() { + return player?.play(); } - Future onPause() async { - await player?.pause(); + Future? onPause() async { + return player?.pause(); } - Future onSeek(Duration duration) async { - await player?.seek(duration); + Future? onSeek(Duration duration) { + return player?.seek(duration); } void _updateCurrItem(DetailItem item) { @@ -297,26 +298,30 @@ class AudioController extends GetxController false, ); if (completed) { - switch (playMode.value) { - case PlayRepeat.pause: - break; - case PlayRepeat.listOrder: - playNext(nextPart: true); - break; - case PlayRepeat.singleCycle: - _replay(); - break; - case PlayRepeat.listCycle: - if (!playNext(nextPart: true)) { - if (index != null && index != 0 && playlist != null) { - playIndex(0); - } else { - _replay(); + if (shutdownTimerService.isWaiting) { + shutdownTimerService.handleWaiting(); + } else { + switch (playMode.value) { + case PlayRepeat.pause: + break; + case PlayRepeat.listOrder: + playNext(nextPart: true); + break; + case PlayRepeat.singleCycle: + _replay(); + break; + case PlayRepeat.listCycle: + if (!playNext(nextPart: true)) { + if (index != null && index != 0 && playlist != null) { + playIndex(0); + } else { + _replay(); + } } - } - break; - case PlayRepeat.autoPlayRelated: - break; + break; + case PlayRepeat.autoPlayRelated: + break; + } } } }), @@ -673,17 +678,6 @@ class AudioController extends GetxController } } - // Timer? _timer; - - // void _cancelTimer() { - // _timer?.cancel(); - // _timer = null; - // } - - // void showTimerDialog() { - // // TODO - // } - @override (Object, int) get getFavRidType => (oid, isVideo ? 2 : 12); @@ -724,7 +718,10 @@ class AudioController extends GetxController @override void onClose() { - // _cancelTimer(); + shutdownTimerService + ..onPause = null + ..isPlaying = null + ..reset(); videoPlayerServiceHandler ?..onPlay = null ..onPause = null diff --git a/lib/pages/audio/view.dart b/lib/pages/audio/view.dart index daab83514..dafe6bbea 100644 --- a/lib/pages/audio/view.dart +++ b/lib/pages/audio/view.dart @@ -12,6 +12,7 @@ import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/pages/audio/controller.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; +import 'package:PiliPlus/services/shutdown_timer_service.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; @@ -90,7 +91,7 @@ class _AudioPageState extends State { builder: (context) { return PopupMenuButton( tooltip: '排序', - icon: const Icon(Icons.sort), + icon: const Icon(Icons.sort, size: 22), initialValue: _controller.order, onSelected: (value) { _controller.onChangeOrder(value); @@ -102,10 +103,22 @@ class _AudioPageState extends State { ); }, ), + IconButton( + tooltip: '定时关闭', + onPressed: () => shutdownTimerService + ..onPause ??= _controller.onPause + ..isPlaying ??= (() => _controller.player?.state.playing ?? false) + ..showScheduleExitDialog( + context, + isFullScreen: false, + ), + icon: const Icon(Icons.schedule, size: 22), + ), if (_controller.isVideo) IconButton( + tooltip: '更多', onPressed: _showMore, - icon: const Icon(Icons.more_vert), + icon: const Icon(Icons.more_vert, size: 22), ), const SizedBox(width: 5), ], diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index ec16ef5b5..9e6a3db15 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -79,11 +79,13 @@ class _PlDanmakuState extends State { } // 播放器状态监听 - void playerListener(PlayerStatus? status) { - if (status == PlayerStatus.playing) { - _controller?.resume(); - } else { - _controller?.pause(); + void playerListener(PlayerStatus status) { + if (_controller case final controller?) { + if (status.isPlaying) { + controller.resume(); + } else { + controller.pause(); + } } } @@ -97,7 +99,7 @@ class _PlDanmakuState extends State { return; } - if (!playerController.playerStatus.playing) { + if (!playerController.playerStatus.isPlaying) { return; } diff --git a/lib/pages/dynamics_create/view.dart b/lib/pages/dynamics_create/view.dart index c92df8516..c208db156 100644 --- a/lib/pages/dynamics_create/view.dart +++ b/lib/pages/dynamics_create/view.dart @@ -8,8 +8,8 @@ import 'package:PiliPlus/common/widgets/flutter/draggable_sheet/draggable_scroll as dyn_sheet; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; -import 'package:PiliPlus/common/widgets/flutter/time_picker.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; +import 'package:PiliPlus/common/widgets/time_picker.dart'; import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart'; diff --git a/lib/pages/dynamics_create_reserve/view.dart b/lib/pages/dynamics_create_reserve/view.dart index fc54f12e3..28f443245 100644 --- a/lib/pages/dynamics_create_reserve/view.dart +++ b/lib/pages/dynamics_create_reserve/view.dart @@ -1,4 +1,4 @@ -import 'package:PiliPlus/common/widgets/flutter/time_picker.dart'; +import 'package:PiliPlus/common/widgets/time_picker.dart'; import 'package:PiliPlus/pages/dynamics_create_reserve/controller.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; diff --git a/lib/pages/dynamics_create_vote/view.dart b/lib/pages/dynamics_create_vote/view.dart index ece157d37..f7610a54d 100644 --- a/lib/pages/dynamics_create_vote/view.dart +++ b/lib/pages/dynamics_create_vote/view.dart @@ -1,8 +1,8 @@ import 'dart:io' show File; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; -import 'package:PiliPlus/common/widgets/flutter/time_picker.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/time_picker.dart'; import 'package:PiliPlus/models/dynamics/vote_model.dart'; import 'package:PiliPlus/pages/dynamics_create_vote/controller.dart'; import 'package:PiliPlus/utils/date_utils.dart'; diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index ff7cc74d7..92e34bf81 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -39,6 +39,7 @@ class _HotPageState extends CommonPageState onTap: onTap, behavior: HitTestBehavior.opaque, child: Column( + spacing: 4, mainAxisSize: MainAxisSize.min, children: [ NetworkImgLayer( @@ -47,7 +48,6 @@ class _HotPageState extends CommonPageState type: .emote, src: iconUrl, ), - const SizedBox(height: 4), Text( title, style: const TextStyle(fontSize: 12), @@ -67,61 +67,53 @@ class _HotPageState extends CommonPageState physics: const AlwaysScrollableScrollPhysics(), controller: controller.scrollController, slivers: [ - SliverToBoxAdapter( - child: Pref.showHotRcmd - ? Padding( - padding: const .only(left: 12, top: 12, right: 12), - child: Row( - mainAxisAlignment: .spaceEvenly, - children: [ - _buildEntranceItem( - iconUrl: - 'https://i0.hdslb.com/bfs/archive/a3f11218aaf4521b4967db2ae164ecd3052586b9.png', - title: '排行榜', - onTap: () { - try { - HomeController homeController = - Get.find(); - int index = homeController.tabs.indexOf( - HomeTabType.rank, - ); - if (index != -1) { - homeController.tabController.animateTo( - index, - ); - } else { - Get.to( - Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: const Text('排行榜'), - ), - body: const ViewSafeArea( - child: RankPage(), - ), - ), - ); - } - } catch (_) {} - }, - ), - _buildEntranceItem( - iconUrl: - 'https://i0.hdslb.com/bfs/archive/552ebe8c4794aeef30ebd1568b59ad35f15e21ad.png', - title: '每周必看', - onTap: () => Get.toNamed('/popularSeries'), - ), - _buildEntranceItem( - iconUrl: - 'https://i0.hdslb.com/bfs/archive/3693ec9335b78ca57353ac0734f36a46f3d179a9.png', - title: '入站必刷', - onTap: () => Get.toNamed('/popularPrecious'), - ), - ], + if (Pref.showHotRcmd) + SliverToBoxAdapter( + child: Padding( + padding: const .only(left: 12, top: 12, right: 12), + child: Row( + mainAxisAlignment: .spaceEvenly, + children: [ + _buildEntranceItem( + iconUrl: + 'https://i0.hdslb.com/bfs/archive/a3f11218aaf4521b4967db2ae164ecd3052586b9.png', + title: '排行榜', + onTap: () { + try { + final homeController = Get.find(); + final index = homeController.tabs.indexOf( + HomeTabType.rank, + ); + if (index != -1) { + homeController.tabController.animateTo(index); + } else { + Get.to( + Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar(title: const Text('排行榜')), + body: const ViewSafeArea(child: RankPage()), + ), + ); + } + } catch (_) {} + }, ), - ) - : const SizedBox.shrink(), - ), + _buildEntranceItem( + iconUrl: + 'https://i0.hdslb.com/bfs/archive/552ebe8c4794aeef30ebd1568b59ad35f15e21ad.png', + title: '每周必看', + onTap: () => Get.toNamed('/popularSeries'), + ), + _buildEntranceItem( + iconUrl: + 'https://i0.hdslb.com/bfs/archive/3693ec9335b78ca57353ac0734f36a46f3d179a9.png', + title: '入站必刷', + onTap: () => Get.toNamed('/popularPrecious'), + ), + ], + ), + ), + ), SliverPadding( padding: const EdgeInsets.only(top: 7, bottom: 100), sliver: Obx( diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 3d229254e..e6a83f92c 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -104,7 +104,7 @@ class _LiveRoomPageState extends State ..danmakuController = _liveRoomController.danmakuController; PlPlayerController.setPlayCallBack(plPlayerController.play); _liveRoomController.startLiveTimer(); - if (plPlayerController.playerStatus.playing && + if (plPlayerController.playerStatus.isPlaying && plPlayerController.cid == null) { _liveRoomController ..danmakuController?.resume() @@ -132,12 +132,12 @@ class _LiveRoomPageState extends State ..danmakuController?.pause() ..cancelLiveTimer() ..closeLiveMsg() - ..isPlaying = plPlayerController.playerStatus.playing; + ..isPlaying = plPlayerController.playerStatus.isPlaying; super.didPushNext(); } - void playerListener(PlayerStatus? status) { - if (status == PlayerStatus.playing) { + void playerListener(PlayerStatus status) { + if (status.isPlaying) { _liveRoomController ..danmakuController?.resume() ..startLiveTimer() diff --git a/lib/pages/live_room/widgets/header_control.dart b/lib/pages/live_room/widgets/header_control.dart index 1b1cad040..068fdaaff 100644 --- a/lib/pages/live_room/widgets/header_control.dart +++ b/lib/pages/live_room/widgets/header_control.dart @@ -5,7 +5,8 @@ import 'package:PiliPlus/pages/live_room/controller.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'; -import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:PiliPlus/services/shutdown_timer_service.dart' + show shutdownTimerService; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; @@ -218,7 +219,11 @@ class _LiveHeaderControlState extends State ComBtn( height: 30, tooltip: '定时关闭', - onTap: () => PageUtils.scheduleExit(context, isFullScreen, true), + onTap: () => shutdownTimerService.showScheduleExitDialog( + context, + isFullScreen: isFullScreen, + isLive: true, + ), icon: const Icon( size: 18, Icons.schedule, diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index c6f66d8dd..35b5f7934 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -172,18 +172,19 @@ class _MainAppState extends PopScopeState void _onHideWindow() { if (_mainController.pauseOnMinimize) { - _mainController.isPlaying = - PlPlayerController.instance?.playerStatus.value == - PlayerStatus.playing; - PlPlayerController.pauseIfExists(); + if (PlPlayerController.instance case final player?) { + if (_mainController.isPlaying = player.playerStatus.isPlaying) { + player.pause(); + } + } else { + _mainController.isPlaying = false; + } } } void _onShowWindow() { - if (_mainController.pauseOnMinimize) { - if (_mainController.isPlaying) { - PlPlayerController.playIfExists(); - } + if (_mainController.pauseOnMinimize && _mainController.isPlaying) { + PlPlayerController.instance?.play(); } } diff --git a/lib/pages/setting/models/model.dart b/lib/pages/setting/models/model.dart index fc7d443fe..71124a5aa 100644 --- a/lib/pages/setting/models/model.dart +++ b/lib/pages/setting/models/model.dart @@ -238,9 +238,7 @@ SettingsModel getVideoFilterSelectModel({ (values ..addIf(!values.contains(value), value) ..sort()) - .map( - (e) => (e, suffix == null ? e.toString() : '$e $suffix'), - ) + .map((e) => (e, suffix == null ? e.toString() : '$e $suffix')) .toList() ..add((-1, '自定义')), ), @@ -285,8 +283,8 @@ SettingsModel getVideoFilterSelectModel({ if (result != -1) { value = result!; setState(); - onChanged?.call(result!); - GStorage.setting.put(key, result); + onChanged?.call(value); + GStorage.setting.put(key, value); } } }, diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 6bdb5c8c8..cb26cdf24 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -278,7 +278,7 @@ class VideoDetailController extends GetxController late final watchProgress = GStorage.watchProgress; void cacheLocalProgress() { - if (plPlayerController.playerStatus.completed) { + if (plPlayerController.playerStatus.isCompleted) { watchProgress.put(cid.value.toString(), entry.totalTimeMilli); } else if (playedTime case final playedTime?) { watchProgress.put(cid.value.toString(), playedTime.inMilliseconds); @@ -1034,7 +1034,8 @@ class VideoDetailController extends GetxController SmartDialog.showToast('UP主已关闭弹幕'); return; } - final isPlaying = autoPlay.value && plPlayerController.playerStatus.playing; + final isPlaying = + autoPlay.value && plPlayerController.playerStatus.isPlaying; if (isPlaying) { await plPlayerController.pause(); } @@ -1630,7 +1631,7 @@ class VideoDetailController extends GetxController void makeHeartBeat() { if (plPlayerController.enableHeart && - !plPlayerController.playerStatus.completed && + !plPlayerController.playerStatus.isCompleted && playedTime != null) { try { plPlayerController.makeHeartBeat( diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index c43933c91..e6bed0d72 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -46,7 +46,8 @@ import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; import 'package:PiliPlus/plugin/pl_player/view.dart'; import 'package:PiliPlus/services/service_locator.dart'; -import 'package:PiliPlus/services/shutdown_timer_service.dart'; +import 'package:PiliPlus/services/shutdown_timer_service.dart' + show shutdownTimerService; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart'; @@ -211,7 +212,7 @@ class _VideoDetailPageVState extends State // 播放器状态监听 Future playerListener(PlayerStatus status) async { - bool isPlaying = status == PlayerStatus.playing; + final isPlaying = status.isPlaying; try { if (videoDetailController.scrollCtr.hasClients) { if (isPlaying) { @@ -236,7 +237,7 @@ class _VideoDetailPageVState extends State if (kDebugMode) debugPrint('handle player status: $e'); } - if (status == PlayerStatus.completed) { + if (status.isCompleted) { try { if (videoDetailController .steinEdgeInfo @@ -250,36 +251,38 @@ class _VideoDetailPageVState extends State return; } } catch (_) {} - shutdownTimerService.handleWaitingFinished(); - bool notExitFlag = false; + + bool exitFlag = true; /// 顺序播放 列表循环 - if (plPlayerController!.playRepeat != PlayRepeat.pause && - plPlayerController!.playRepeat != PlayRepeat.singleCycle) { - notExitFlag = introController.nextPlay(); - } - - /// 单个循环 - if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) { - notExitFlag = true; - plPlayerController!.play(repeat: true); - } - - // 结束播放退出全屏 - if (!notExitFlag && autoExitFullscreen) { - plPlayerController!.triggerFullScreen(status: false); - if (plPlayerController!.longPressStatus.value) { - plPlayerController!.setLongPressStatus(false); - } - if (plPlayerController!.controlsLock.value) { - plPlayerController!.onLockControl(false); + if (shutdownTimerService.isWaiting) { + shutdownTimerService.handleWaiting(); + } else { + switch (plPlayerController!.playRepeat) { + case PlayRepeat.singleCycle: + exitFlag = false; + plPlayerController!.play(repeat: true); + case PlayRepeat.listOrder: + case PlayRepeat.listCycle: + case PlayRepeat.autoPlayRelated: + exitFlag = !introController.nextPlay(); + case PlayRepeat.pause: } } - // 播放完展示控制栏 - if (Platform.isAndroid && !notExitFlag) { - PiPStatus currentStatus = await Floating().pipStatus; - if (currentStatus == PiPStatus.disabled) { - plPlayerController!.onLockControl(false); + + if (exitFlag) { + // 结束播放退出全屏 + if (autoExitFullscreen) { + plPlayerController!.triggerFullScreen(status: false); + if (plPlayerController!.controlsLock.value) { + plPlayerController!.onLockControl(false); + } + } + // 播放完展示控制栏 + if (Platform.isAndroid) { + if (await Floating().pipStatus == PiPStatus.disabled) { + plPlayerController!.onLockControl(false); + } } } } @@ -351,7 +354,6 @@ class _VideoDetailPageVState extends State if (!videoDetailController.horizontalScreen) { AutoOrientation.portraitUpMode(); } - shutdownTimerService.handleWaitingFinished(); if (!videoDetailController.plPlayerController.isCloseAll) { videoPlayerServiceHandler?.onVideoDetailDispose(heroTag); if (plPlayerController != null) { @@ -416,7 +418,7 @@ class _VideoDetailPageVState extends State WidgetsBinding.instance.addObserver(this); plPlayerController?.isLive = false; - if (videoDetailController.plPlayerController.playerStatus.playing && + if (videoDetailController.plPlayerController.playerStatus.isPlaying && videoDetailController.playerStatus != PlayerStatus.playing) { videoDetailController.plPlayerController.pause(); } @@ -447,7 +449,7 @@ class _VideoDetailPageVState extends State () async { if (videoDetailController.autoPlay.value) { await videoDetailController.playerInit( - autoplay: videoDetailController.playerStatus == PlayerStatus.playing, + autoplay: videoDetailController.playerStatus?.isPlaying ?? false, ); } else if (videoDetailController.plPlayerController.preInitPlayer && !videoDetailController.isQuerying && @@ -622,8 +624,7 @@ class _VideoDetailPageVState extends State videoDetailController.isCollapsing ? animHeight : videoDetailController.isCollapsing || - plPlayerController?.playerStatus.value == - PlayerStatus.playing + (plPlayerController?.playerStatus.isPlaying ?? false) ? videoDetailController.minVideoHeight : kToolbarHeight; if (videoDetailController.isExpanding && @@ -738,7 +739,7 @@ class _VideoDetailPageVState extends State Text( '${videoDetailController.playedTime == null ? '立即' - : plPlayerController!.playerStatus.completed + : plPlayerController!.playerStatus.isCompleted ? '重新' : '继续'}播放', style: TextStyle( diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 59d3f14a1..7f26516ed 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -36,6 +36,8 @@ import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; import 'package:PiliPlus/services/service_locator.dart'; +import 'package:PiliPlus/services/shutdown_timer_service.dart' + show shutdownTimerService; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; @@ -418,7 +420,10 @@ class HeaderControlState extends State dense: true, onTap: () { Get.back(); - PageUtils.scheduleExit(this.context, isFullScreen); + shutdownTimerService.showScheduleExitDialog( + this.context, + isFullScreen: isFullScreen, + ); }, leading: const Icon(Icons.hourglass_top_outlined, size: 20), title: const Text('定时关闭', style: titleStyle), diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 474f10b90..a87cc9e18 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -525,7 +525,7 @@ class PlPlayerController { bool notify = true, bool isInterrupt = false, }) async { - if (_instance?.playerStatus.value == PlayerStatus.playing) { + if (_instance?.playerStatus.isPlaying ?? false) { await _instance?.pause(notify: notify, isInterrupt: isInterrupt); } } @@ -558,7 +558,7 @@ class PlPlayerController { if (sdkInt < 36) { Utils.channel.setMethodCallHandler((call) async { if (call.method == 'onUserLeaveHint') { - if (playerStatus.playing && _isCurrVideoPage) { + if (playerStatus.isPlaying && _isCurrVideoPage) { enterPip(); } } @@ -1215,7 +1215,7 @@ class PlPlayerController { } catch (e) { if (kDebugMode) debugPrint('seek failed: $e'); } - // if (playerStatus.value == PlayerStatus.paused) { + // if (playerStatus.isPaused) { // play(); // } t.cancel(); @@ -1437,7 +1437,7 @@ class PlPlayerController { return; } if (val) { - if (playerStatus.value == PlayerStatus.playing) { + if (playerStatus.isPlaying) { longPressStatus.value = val; HapticFeedback.lightImpact(); await setPlaybackSpeed( @@ -1634,14 +1634,13 @@ class PlPlayerController { } if (!enableHeart || MineController.anonymity.value || progress == 0) { return; - } else if (playerStatus.value == PlayerStatus.paused) { + } else if (playerStatus.isPaused) { if (!isManual) { return; } } bool isComplete = - playerStatus.value == PlayerStatus.completed || - type == HeartBeatType.completed; + playerStatus.isCompleted || type == HeartBeatType.completed; if ((durationSeconds.value - position.value).inMilliseconds > 1000) { isComplete = false; } @@ -1741,7 +1740,7 @@ class PlPlayerController { subscriptions.clear(); _positionListeners.clear(); _statusListeners.clear(); - if (playerStatus.playing) { + if (playerStatus.isPlaying) { WakelockPlus.disable(); } _videoPlayerController?.dispose(); diff --git a/lib/plugin/pl_player/models/play_status.dart b/lib/plugin/pl_player/models/play_status.dart index ddd7f3e3e..f0a5a9d0a 100644 --- a/lib/plugin/pl_player/models/play_status.dart +++ b/lib/plugin/pl_player/models/play_status.dart @@ -1,19 +1,20 @@ import 'package:get/get.dart'; -enum PlayerStatus { completed, playing, paused } +enum PlayerStatus { + completed, + playing, + paused + ; + + bool get isCompleted => this == PlayerStatus.completed; + bool get isPlaying => this == PlayerStatus.playing; + bool get isPaused => this == PlayerStatus.paused; +} typedef PlPlayerStatus = Rx; extension PlPlayerStatusExt on PlPlayerStatus { - bool get playing { - return value == PlayerStatus.playing; - } - - bool get paused { - return value == PlayerStatus.paused; - } - - bool get completed { - return value == PlayerStatus.completed; - } + bool get isPlaying => value.isPlaying; + bool get isPaused => value.isPaused; + bool get isCompleted => value.isCompleted; } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index f36e99aac..98e39d9d6 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -1899,7 +1899,7 @@ class _PLVideoPlayerState extends State Obx(() { if (plPlayerController.dataStatus.loading || (plPlayerController.isBuffering.value && - plPlayerController.playerStatus.playing)) { + plPlayerController.playerStatus.isPlaying)) { return Center( child: GestureDetector( onTap: plPlayerController.refreshPlayer, @@ -2110,7 +2110,7 @@ class _PLVideoPlayerState extends State category: SegmentType.sponsor, actionType: ActionType.skip, ); - final isPlay = ctr.playerStatus.playing; + final isPlay = ctr.playerStatus.isPlaying; if (isPlay) ctr.pause(); WebpPreset preset = WebpPreset.def; diff --git a/lib/services/audio_handler.dart b/lib/services/audio_handler.dart index 7222d39b0..aba5f3af3 100644 --- a/lib/services/audio_handler.dart +++ b/lib/services/audio_handler.dart @@ -36,9 +36,9 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { static final List _item = []; bool enableBackgroundPlay = Pref.enableBackgroundPlay; - Future Function()? onPlay; - Future Function()? onPause; - Future Function(Duration position)? onSeek; + Future? Function()? onPlay; + Future? Function()? onPause; + Future? Function(Duration position)? onSeek; @override Future play() { @@ -89,8 +89,7 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { } final AudioProcessingState processingState; - final playing = status == PlayerStatus.playing; - if (status == PlayerStatus.completed) { + if (status.isCompleted) { processingState = AudioProcessingState.completed; } else if (isBuffering) { processingState = AudioProcessingState.buffering; @@ -98,6 +97,7 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { processingState = AudioProcessingState.ready; } + final playing = status.isPlaying; playbackState.add( playbackState.value.copyWith( processingState: isBuffering diff --git a/lib/services/shutdown_timer_service.dart b/lib/services/shutdown_timer_service.dart index 51b96b1a0..cc9b97106 100644 --- a/lib/services/shutdown_timer_service.dart +++ b/lib/services/shutdown_timer_service.dart @@ -2,167 +2,260 @@ import 'dart:async'; import 'dart:io'; +import 'package:PiliPlus/models/common/enum_with_label.dart'; +import 'package:PiliPlus/pages/video/introduction/ugc/widgets/menu_row.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; +import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -class ShutdownTimerService with WidgetsBindingObserver { - static final ShutdownTimerService _instance = - ShutdownTimerService._internal(); - Timer? _shutdownTimer; - Timer? _autoCloseDialogTimer; - //定时退出 - int scheduledExitInMinutes = 0; - bool exitApp = false; - bool waitForPlayingCompleted = false; - bool isWaiting = false; - bool isInBackground = false; - factory ShutdownTimerService() => _instance; - - ShutdownTimerService._internal() { - WidgetsBinding.instance.addObserver(this); // 添加观察者 - } - - void dispose() { - WidgetsBinding.instance.removeObserver(this); // 移除观察者 - _shutdownTimer?.cancel(); - _autoCloseDialogTimer?.cancel(); - _instance.dispose(); - } +enum _ShutdownType with EnumWithLabel { + pause('暂停视频'), + exit('退出APP') + ; @override - void didChangeAppLifecycleState(AppLifecycleState state) { - isInBackground = state == AppLifecycleState.paused; - } - - void startShutdownTimer() { - cancelShutdownTimer(); // Cancel any previous timer - if (scheduledExitInMinutes == 0) { - //使用toast提示用户已取消 - SmartDialog.showToast("取消定时关闭"); - return; - } - SmartDialog.showToast("设置 $scheduledExitInMinutes 分钟后定时关闭"); - _shutdownTimer = Timer( - Duration(minutes: scheduledExitInMinutes), - _shutdownDecider, - ); - } - - void _showTimeUpButPauseDialog() { - SmartDialog.show( - builder: (dialogContext) => AlertDialog( - title: const Text('定时关闭'), - content: const Text('时间到啦!'), - actions: [ - TextButton( - child: const Text('确认'), - onPressed: () { - cancelShutdownTimer(); - SmartDialog.dismiss(); - }, - ), - ], - ), - ); - } - - void _showShutdownDialog() { - if (isInBackground) { - // if (kDebugMode) debugPrint("app在后台运行,不弹窗"); - _executeShutdown(); - return; - } - SmartDialog.show( - builder: (BuildContext dialogContext) { - // Start the 10-second timer to auto close the dialog - _autoCloseDialogTimer?.cancel(); - _autoCloseDialogTimer = Timer(const Duration(seconds: 10), () { - SmartDialog.dismiss(); // Close the dialog - _executeShutdown(); - }); - return AlertDialog( - title: const Text('定时关闭'), - content: const Text('将在10秒后执行,是否需要取消?'), - actions: [ - TextButton( - child: const Text('取消关闭'), - onPressed: () { - _autoCloseDialogTimer?.cancel(); // Cancel the auto-close timer - cancelShutdownTimer(); // Cancel the shutdown timer - SmartDialog.dismiss(); // Close the dialog - }, - ), - ], - ); - }, - ).whenComplete(() { - // Cleanup when the dialog is dismissed - _autoCloseDialogTimer?.cancel(); - }); - } - - void _shutdownDecider() { - if (exitApp && !waitForPlayingCompleted) { - _showShutdownDialog(); - return; - } - // PlPlayerController plPlayerController = PlPlayerController.getInstance(); - PlayerStatus? playerStatus = PlPlayerController.getPlayerStatusIfExists(); - if (!exitApp && !waitForPlayingCompleted) { - // if (!plPlayerController.playerStatus.playing) { - if (playerStatus == PlayerStatus.paused || - playerStatus == PlayerStatus.completed) { - //仅提示用户 - _showTimeUpButPauseDialog(); - } else { - _showShutdownDialog(); - } - return; - } - //waitForPlayingCompleted - if (playerStatus == PlayerStatus.paused || - playerStatus == PlayerStatus.completed) { - // if (!plPlayerController.playerStatus.playing) { - _showShutdownDialog(); - return; - } - SmartDialog.showToast("定时关闭时间已到,等待当前视频播放完成"); - //监听播放完成 - //该方法依赖耦合实现,不够优雅 - isWaiting = true; - } - - void handleWaitingFinished() { - if (isWaiting) { - _showShutdownDialog(); - isWaiting = false; - } - } - - void _executeShutdown() { - if (exitApp) { - PlPlayerController.pauseIfExists(); - //退出app - exit(0); - } else { - //暂停播放 - PlayerStatus? playerStatus = PlPlayerController.getPlayerStatusIfExists(); - if (playerStatus == PlayerStatus.playing) { - PlPlayerController.pauseIfExists(); - waitForPlayingCompleted = true; - SmartDialog.showToast("已暂停播放"); - } else { - SmartDialog.showToast("当前未播放"); - } - } - } - - void cancelShutdownTimer() { - isWaiting = false; - _shutdownTimer?.cancel(); - } + final String label; + const _ShutdownType(this.label); } final shutdownTimerService = ShutdownTimerService(); + +class ShutdownTimerService { + ShutdownTimerService._internal(); + factory ShutdownTimerService() => _instance; + static final ShutdownTimerService _instance = + ShutdownTimerService._internal(); + + VoidCallback? onPause; + ValueGetter? isPlaying; + + Timer? _shutdownTimer; + int _durationInMinutes = 0; + _ShutdownType _shutdownType = .pause; + + bool? _isWaiting; + bool get isWaiting => _isWaiting ?? false; + bool _waitUntilCompleted = false; + + void _stopTimer() { + if (_shutdownTimer != null) { + _shutdownTimer!.cancel(); + _shutdownTimer = null; + } + } + + void reset([int durationInMinutes = 0]) { + _stopTimer(); + _isWaiting = null; + _durationInMinutes = durationInMinutes; + } + + void _startShutdownTimer(int durationInMinutes) { + reset(durationInMinutes); + if (durationInMinutes == 0) { + SmartDialog.showToast('取消定时关闭'); + return; + } + SmartDialog.showToast('设置 ${_format(durationInMinutes)} 后定时关闭'); + _shutdownTimer = Timer( + Duration(minutes: durationInMinutes), + _handleShutdown, + ); + } + + void _handleShutdown() { + switch (_shutdownType) { + case _ShutdownType.pause: + late final player = PlPlayerController.instance; + final isPlaying = + this.isPlaying?.call() ?? player?.playerStatus.isPlaying ?? false; + if (isPlaying) { + if (_waitUntilCompleted) { + _isWaiting = true; + } else { + _durationInMinutes = 0; + (onPause ?? player?.pause)?.call(); + SmartDialog.showToast('定时时间已到,已暂停'); + } + } + case _ShutdownType.exit: + if (_waitUntilCompleted) { + final isPlaying = + this.isPlaying?.call() ?? + PlPlayerController.instance?.playerStatus.isPlaying ?? + false; + if (isPlaying) { + _isWaiting = true; + return; + } + } + exit(0); + } + } + + void handleWaiting() { + switch (_shutdownType) { + case _ShutdownType.pause: + _isWaiting = null; + _durationInMinutes = 0; + SmartDialog.showToast('定时时间已到,已暂停'); + case _ShutdownType.exit: + exit(0); + } + } + + static (int hour, int minute) _parseMinutes(int minutes) => + (minutes ~/ 60, minutes % 60); + + static String _format(int minutes) { + if (minutes == 60) return '60分钟'; + final (int hour, int minute) = _parseMinutes(minutes); + if (hour > 0 && minute > 0) { + return '$hour小时$minute分钟'; + } else if (hour > 0) { + return '$hour小时'; + } else { + return '$minute分钟'; + } + } + + void showScheduleExitDialog( + BuildContext context, { + required bool isFullScreen, + bool isLive = false, + }) { + const Set scheduleTimeMinutes = {0, 15, 30, 45, 60}; + const TextStyle titleStyle = TextStyle(fontSize: 14); + if (isLive) { + _waitUntilCompleted = false; + } + PageUtils.showVideoBottomSheet( + context, + isFullScreen: () => isFullScreen, + child: StatefulBuilder( + builder: (_, setState) { + final ThemeData theme = Theme.of(context); + return Theme( + data: theme, + child: Padding( + padding: const .all(12), + child: Material( + clipBehavior: .hardEdge, + color: theme.colorScheme.surface, + borderRadius: const .all(.circular(12)), + child: ListView( + padding: const .symmetric(vertical: 14), + children: [ + const Center(child: Text('定时关闭', style: titleStyle)), + const SizedBox(height: 10), + ...{...scheduleTimeMinutes, _durationInMinutes} + .sorted((a, b) => a.compareTo(b)) + .map( + (minutes) => ListTile( + dense: true, + onTap: () { + Navigator.pop(context); + _startShutdownTimer(minutes); + }, + title: Text( + switch (minutes) { + 0 => '禁用', + _ => _format(minutes), + }, + style: titleStyle, + ), + trailing: _durationInMinutes == minutes + ? Icon( + size: 20, + Icons.done, + color: theme.colorScheme.primary, + ) + : null, + ), + ), + ListTile( + dense: true, + onTap: () { + final (int hour, int minute) = _parseMinutes( + _durationInMinutes, + ); + showTimePicker( + context: context, + initialEntryMode: .inputOnly, + initialTime: TimeOfDay(hour: hour, minute: minute), + builder: (context, child) => MediaQuery( + data: MediaQuery.of( + context, + ).copyWith(alwaysUse24HourFormat: true), + child: child!, + ), + ).then((time) { + if (time != null) { + _startShutdownTimer(time.hour * 60 + time.minute); + setState(() {}); + } + }); + }, + title: const Text('自定义', style: titleStyle), + ), + if (!isLive) ...[ + Builder( + builder: (context) { + void onChanged([_]) { + _waitUntilCompleted = !_waitUntilCompleted; + (context as Element).markNeedsBuild(); + } + + return ListTile( + dense: true, + onTap: onChanged, + title: const Text('额外等待视频播放完毕', style: titleStyle), + trailing: Transform.scale( + alignment: Alignment.centerRight, + scale: 0.8, + child: Switch( + value: _waitUntilCompleted, + onChanged: onChanged, + ), + ), + ); + }, + ), + ], + const SizedBox(height: 5), + Padding( + padding: const .only(left: 18), + child: Builder( + builder: (context) { + return Row( + spacing: 12, + children: [ + const Text('倒计时结束:', style: titleStyle), + ..._ShutdownType.values.map( + (e) => ActionRowLineItem( + onTap: () { + _shutdownType = e; + (context as Element).markNeedsBuild(); + }, + text: ' ${e.label} ', + selectStatus: _shutdownType == e, + ), + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/utils/page_utils.dart b/lib/utils/page_utils.dart index cd60c3232..3a5dbb1fd 100644 --- a/lib/utils/page_utils.dart +++ b/lib/utils/page_utils.dart @@ -16,8 +16,6 @@ import 'package:PiliPlus/pages/common/publish/publish_route.dart'; import 'package:PiliPlus/pages/contact/view.dart'; import 'package:PiliPlus/pages/fav_panel/view.dart'; import 'package:PiliPlus/pages/share/view.dart'; -import 'package:PiliPlus/pages/video/introduction/ugc/widgets/menu_row.dart'; -import 'package:PiliPlus/services/shutdown_timer_service.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/extension.dart'; @@ -34,7 +32,6 @@ import 'package:PiliPlus/utils/utils.dart'; import 'package:floating/floating.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show FilteringTextInputFormatter; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -110,188 +107,6 @@ abstract final class PageUtils { } } - static void scheduleExit( - BuildContext context, - isFullScreen, [ - bool isLive = false, - ]) { - if (!context.mounted) { - return; - } - const List scheduleTimeChoices = [0, 15, 30, 45, 60]; - const TextStyle titleStyle = TextStyle(fontSize: 14); - if (isLive) { - shutdownTimerService.waitForPlayingCompleted = false; - } - showVideoBottomSheet( - context, - isFullScreen: () => isFullScreen, - child: StatefulBuilder( - builder: (_, setState) { - void onTap(int choice) { - if (choice == -1) { - String duration = ''; - showDialog( - context: context, - builder: (context) { - final theme = Theme.of(context); - return AlertDialog( - title: const Text('自定义时长'), - content: TextField( - autofocus: true, - onChanged: (value) => duration = value, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - decoration: const InputDecoration(suffixText: 'min'), - ), - actions: [ - TextButton( - onPressed: Get.back, - child: Text( - '取消', - style: TextStyle(color: theme.colorScheme.outline), - ), - ), - TextButton( - onPressed: () { - try { - final choice = int.parse(duration); - Get.back(); - shutdownTimerService - ..scheduledExitInMinutes = choice - ..startShutdownTimer(); - setState(() {}); - } catch (e) { - SmartDialog.showToast(e.toString()); - } - }, - child: const Text('确定'), - ), - ], - ); - }, - ); - } else { - Get.back(); - shutdownTimerService.scheduledExitInMinutes = choice; - shutdownTimerService.startShutdownTimer(); - } - } - - final ThemeData theme = Theme.of(context); - return Theme( - data: theme, - child: Padding( - padding: const EdgeInsets.all(12), - child: Material( - clipBehavior: Clip.hardEdge, - color: theme.colorScheme.surface, - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 14), - children: [ - const Center(child: Text('定时关闭', style: titleStyle)), - const SizedBox(height: 10), - ...[ - ...[ - ...scheduleTimeChoices, - if (!scheduleTimeChoices.contains( - shutdownTimerService.scheduledExitInMinutes, - )) - shutdownTimerService.scheduledExitInMinutes, - ]..sort(), - -1, - ].map( - (choice) => ListTile( - dense: true, - onTap: () => onTap(choice), - title: Text( - choice == -1 - ? '自定义' - : choice == 0 - ? "禁用" - : "$choice分钟后", - style: titleStyle, - ), - trailing: - shutdownTimerService.scheduledExitInMinutes == - choice - ? Icon( - size: 20, - Icons.done, - color: theme.colorScheme.primary, - ) - : null, - ), - ), - if (!isLive) ...[ - Builder( - builder: (context) { - return ListTile( - dense: true, - onTap: () { - shutdownTimerService.waitForPlayingCompleted = - !shutdownTimerService.waitForPlayingCompleted; - (context as Element).markNeedsBuild(); - }, - title: const Text("额外等待视频播放完毕", style: titleStyle), - trailing: Transform.scale( - alignment: Alignment.centerRight, - scale: 0.8, - child: Switch( - value: shutdownTimerService - .waitForPlayingCompleted, - onChanged: (value) { - shutdownTimerService.waitForPlayingCompleted = - value; - (context as Element).markNeedsBuild(); - }, - ), - ), - ); - }, - ), - ], - const SizedBox(height: 10), - Builder( - builder: (context) { - return Row( - children: [ - const SizedBox(width: 18), - const Text('倒计时结束:', style: titleStyle), - const Spacer(), - ActionRowLineItem( - onTap: () { - shutdownTimerService.exitApp = false; - (context as Element).markNeedsBuild(); - }, - text: " 暂停视频 ", - selectStatus: !shutdownTimerService.exitApp, - ), - const Spacer(), - ActionRowLineItem( - onTap: () { - shutdownTimerService.exitApp = true; - (context as Element).markNeedsBuild(); - }, - text: " 退出APP ", - selectStatus: shutdownTimerService.exitApp, - ), - const SizedBox(width: 25), - ], - ); - }, - ), - ], - ), - ), - ), - ); - }, - ), - ); - } - static Future pushDynFromId({ String? id, Object? rid,