Files
PiliPlus/lib/services/shutdown_timer_service.dart
dom 0cb07aef1c audio block
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-08 23:35:58 +08:00

263 lines
8.8 KiB
Dart

// 定时关闭服务
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';
enum _ShutdownType with EnumWithLabel {
pause('暂停视频'),
exit('退出APP')
;
@override
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<bool>? isPlaying;
Timer? _shutdownTimer;
bool get isActive => _shutdownTimer?.isActive ?? false;
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<int> 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,
),
),
],
);
},
),
),
],
),
),
),
);
},
),
);
}
}