diff --git a/lib/common/widgets/content_container.dart b/lib/common/widgets/content_container.dart index 076a02e9..a9a1bf12 100644 --- a/lib/common/widgets/content_container.dart +++ b/lib/common/widgets/content_container.dart @@ -20,7 +20,7 @@ class ContentContainer extends StatelessWidget { builder: (BuildContext context, BoxConstraints constraints) { return SingleChildScrollView( clipBehavior: childClipBehavior ?? Clip.hardEdge, - physics: isScrollable ? null : NeverScrollableScrollPhysics(), + physics: isScrollable ? null : const NeverScrollableScrollPhysics(), child: ConstrainedBox( constraints: constraints.copyWith( minHeight: constraints.maxHeight, diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index ceaa2648..f3f566f2 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -24,6 +24,7 @@ import 'package:pilipala/services/service_locator.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/plugin/pl_player/utils/fullscreen.dart'; +import '../../../services/shutdown_timer_service.dart'; import 'widgets/header_control.dart'; class VideoDetailPage extends StatefulWidget { @@ -120,7 +121,7 @@ class _VideoDetailPageState extends State if (autoExitFullcreen) { plPlayerController!.triggerFullScreen(status: false); } - + shutdownTimerService.handleWaitingFinished(); /// 顺序播放 列表循环 if (plPlayerController!.playRepeat != PlayRepeat.pause && plPlayerController!.playRepeat != PlayRepeat.singleCycle) { @@ -187,6 +188,7 @@ class _VideoDetailPageState extends State @override void dispose() { + shutdownTimerService.handleWaitingFinished(); if (plPlayerController != null) { plPlayerController!.removeStatusLister(playerListener); plPlayerController!.dispose(); diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index db6aeb3c..f2e38870 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -17,6 +17,7 @@ import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/http/danmaku.dart'; +import 'package:pilipala/services/shutdown_timer_service.dart'; class HeaderControl extends StatefulWidget implements PreferredSizeWidget { const HeaderControl({ @@ -39,14 +40,13 @@ class HeaderControl extends StatefulWidget implements PreferredSizeWidget { class _HeaderControlState extends State { late PlayUrlModel videoInfo; List playSpeed = PlaySpeed.values; - TextStyle subTitleStyle = const TextStyle(fontSize: 12); - TextStyle titleStyle = const TextStyle(fontSize: 14); + static const TextStyle subTitleStyle = TextStyle(fontSize: 12); + static const TextStyle titleStyle = TextStyle(fontSize: 14); Size get preferredSize => const Size(double.infinity, kToolbarHeight); final Box localCache = GStrorage.localCache; final Box videoStorage = GStrorage.video; late List speedsList; double buttonSpace = 8; - @override void initState() { super.initState(); @@ -63,7 +63,7 @@ class _HeaderControlState extends State { builder: (_) { return Container( width: double.infinity, - height: 440, + height: 460, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, @@ -125,13 +125,20 @@ class _HeaderControlState extends State { }, dense: true, leading: const Icon(Icons.watch_later_outlined, size: 20), - title: Text('添加至「稍后再看」', style: titleStyle), + title: const Text('添加至「稍后再看」', style: titleStyle), + ), + ListTile( + onTap: () => {Get.back(), scheduleExit()}, + dense: true, + leading: + const Icon(Icons.hourglass_top_outlined, size: 20), + title: const Text('定时关闭(测试)', style: titleStyle), ), ListTile( onTap: () => {Get.back(), showSetVideoQa()}, dense: true, leading: const Icon(Icons.play_circle_outline, size: 20), - title: Text('选择画质', style: titleStyle), + title: const Text('选择画质', style: titleStyle), subtitle: Text( '当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}', style: subTitleStyle), @@ -141,7 +148,7 @@ class _HeaderControlState extends State { onTap: () => {Get.back(), showSetAudioQa()}, dense: true, leading: const Icon(Icons.album_outlined, size: 20), - title: Text('选择音质', style: titleStyle), + title: const Text('选择音质', style: titleStyle), subtitle: Text( '当前音质 ${widget.videoDetailCtr!.currentAudioQa!.description}', style: subTitleStyle), @@ -150,7 +157,7 @@ class _HeaderControlState extends State { onTap: () => {Get.back(), showSetDecodeFormats()}, dense: true, leading: const Icon(Icons.av_timer_outlined, size: 20), - title: Text('解码格式', style: titleStyle), + title: const Text('解码格式', style: titleStyle), subtitle: Text( '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}', style: subTitleStyle), @@ -159,7 +166,7 @@ class _HeaderControlState extends State { onTap: () => {Get.back(), showSetRepeat()}, dense: true, leading: const Icon(Icons.repeat, size: 20), - title: Text('播放顺序', style: titleStyle), + title: const Text('播放顺序', style: titleStyle), subtitle: Text(widget.controller!.playRepeat.description, style: subTitleStyle), ), @@ -167,7 +174,7 @@ class _HeaderControlState extends State { onTap: () => {Get.back(), showSetDanmaku()}, dense: true, leading: const Icon(Icons.subtitles_outlined, size: 20), - title: Text('弹幕设置', style: titleStyle), + title: const Text('弹幕设置', style: titleStyle), ), ], ), @@ -263,6 +270,133 @@ class _HeaderControlState extends State { ); } + /// 定时关闭 + void scheduleExit() async { + const List scheduleTimeChoices = [ + -1, + 15, + 30, + 60, + ]; + showModalBottomSheet( + context: context, + elevation: 0, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Container( + width: double.infinity, + height: 500, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(12), + padding: const EdgeInsets.only(left: 14, right: 14), + child: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 0, horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 30), + const Center(child: Text('定时关闭', style: titleStyle)), + const SizedBox(height: 10), + for (final int choice in scheduleTimeChoices) ...[ + ListTile( + onTap: () { + shutdownTimerService.scheduledExitInMinutes = + choice; + shutdownTimerService.startShutdownTimer(); + Get.back(); + }, + contentPadding: const EdgeInsets.only(), + dense: true, + title: Text(choice == -1 ? "禁用" : "$choice分钟后"), + trailing: shutdownTimerService + .scheduledExitInMinutes == + choice + ? Icon( + Icons.done, + color: Theme.of(context).colorScheme.primary, + ) + : const SizedBox(), + ) + ], + const SizedBox(height: 6), + const Center( + child: SizedBox( + width: 100, + child: Divider(height: 1), + )), + const SizedBox(height: 10), + ListTile( + onTap: () { + shutdownTimerService.waitForPlayingCompleted = + !shutdownTimerService.waitForPlayingCompleted; + setState(() {}); + }, + dense: true, + contentPadding: const EdgeInsets.only(), + title: + const Text("额外等待视频播放完毕", style: titleStyle), + trailing: Switch( + // thumb color (round icon) + activeColor: Theme.of(context).colorScheme.primary, + activeTrackColor: + Theme.of(context).colorScheme.primaryContainer, + inactiveThumbColor: + Theme.of(context).colorScheme.primaryContainer, + inactiveTrackColor: + Theme.of(context).colorScheme.background, + splashRadius: 10.0, + // boolean variable value + value: shutdownTimerService.waitForPlayingCompleted, + // changes the state of the switch + 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, + ) + ], + ), + ]), + ), + ), + ); + }); + }, + ); + } + /// 选择倍速 void showSetSpeedSheet() { final double currentSpeed = widget.controller!.playbackSpeed; @@ -367,7 +501,7 @@ class _HeaderControlState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('选择画质', style: titleStyle), + const Text('选择画质', style: titleStyle), SizedBox(width: buttonSpace), Icon( Icons.info_outline, @@ -448,7 +582,7 @@ class _HeaderControlState extends State { margin: const EdgeInsets.all(12), child: Column( children: [ - SizedBox( + const SizedBox( height: 45, child: Center(child: Text('选择音质', style: titleStyle))), Expanded( @@ -614,7 +748,7 @@ class _HeaderControlState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 45, child: Center(child: Text('弹幕设置', style: titleStyle)), ), diff --git a/lib/services/shutdown_timer_service.dart b/lib/services/shutdown_timer_service.dart new file mode 100644 index 00000000..aa9c5ceb --- /dev/null +++ b/lib/services/shutdown_timer_service.dart @@ -0,0 +1,140 @@ +// 定时关闭服务 +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; + +import '../plugin/pl_player/controller.dart'; + +class ShutdownTimerService { + static final ShutdownTimerService _instance = + ShutdownTimerService._internal(); + Timer? _shutdownTimer; + Timer? _autoCloseDialogTimer; + //定时退出 + int scheduledExitInMinutes = -1; + bool exitApp = false; + bool waitForPlayingCompleted = false; + bool isWaiting = false; + + factory ShutdownTimerService() => _instance; + + ShutdownTimerService._internal(); + + void startShutdownTimer() { + cancelShutdownTimer(); // Cancel any previous timer + if (scheduledExitInMinutes == -1) { + //使用toast提示用户已取消 + SmartDialog.showToast("取消定时关闭"); + return; + } + SmartDialog.showToast("设置 $scheduledExitInMinutes 分钟后定时关闭"); + _shutdownTimer = Timer(Duration(minutes: scheduledExitInMinutes), + () => _shutdownDecider()); + } + + void _showTimeUpButPauseDialog() { + SmartDialog.show( + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text('定时关闭'), + content: const Text('时间到啦!'), + actions: [ + TextButton( + child: const Text('确认'), + onPressed: () { + cancelShutdownTimer(); + SmartDialog.dismiss(); + }, + ), + ], + ); + }, + ); + } + + void _showShutdownDialog() { + 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 + }, + ), + ], + ); + }, + ).then((_) { + // Cleanup when the dialog is dismissed + _autoCloseDialogTimer?.cancel(); + }); + } + + void _shutdownDecider() { + if (exitApp && !waitForPlayingCompleted) { + _showShutdownDialog(); + return; + } + PlPlayerController plPlayerController = PlPlayerController.getInstance(); + if (!exitApp && !waitForPlayingCompleted) { + if (!plPlayerController.playerStatus.playing) { + //仅提示用户 + _showTimeUpButPauseDialog(); + } else { + _showShutdownDialog(); + } + return; + } + //waitForPlayingCompleted + if (!plPlayerController.playerStatus.playing) { + _showShutdownDialog(); + return; + } + SmartDialog.showToast("定时关闭时间已到,等待当前视频播放完成"); + //监听播放完成 + //该方法依赖耦合实现,不够优雅 + isWaiting = true; + } + void handleWaitingFinished(){ + if(isWaiting){ + _showShutdownDialog(); + isWaiting = false; + } + } + void _executeShutdown() { + if (exitApp) { + //退出app + exit(0); + } else { + //暂停播放 + PlPlayerController plPlayerController = PlPlayerController.getInstance(); + if (plPlayerController.playerStatus.playing) { + plPlayerController.pause(); + waitForPlayingCompleted = true; + SmartDialog.showToast("已暂停播放"); + } else { + SmartDialog.showToast("当前未播放"); + } + } + } + + void cancelShutdownTimer() { + isWaiting = false; + _shutdownTimer?.cancel(); + } +} + +final shutdownTimerService = ShutdownTimerService();