mod: live schedule

Closes #581

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-04-02 18:03:47 +08:00
parent d1a6798f2e
commit 86abf006d0
8 changed files with 378 additions and 293 deletions

View File

@@ -145,24 +145,26 @@ class LiveRoomController extends GetxController {
} }
void liveMsg() { void liveMsg() {
LiveHttp.liveRoomDanmaPrefetch(roomId: roomId).then((v) { if (messages.isEmpty) {
if (v['status']) { LiveHttp.liveRoomDanmaPrefetch(roomId: roomId).then((v) {
messages.addAll((v['data'] as List) if (v['status']) {
.map((obj) => { messages.addAll((v['data'] as List)
'name': obj['user']['base']['name'], .map((obj) => {
'uid': obj['user']['uid'], 'name': obj['user']['base']['name'],
'text': obj['text'], 'uid': obj['user']['uid'],
'emots': obj['emots'], 'text': obj['text'],
'uemote': obj['emoticon']['emoticon_unique'] != "" 'emots': obj['emots'],
? obj['emoticon'] 'uemote': obj['emoticon']['emoticon_unique'] != ""
: null, ? obj['emoticon']
}) : null,
.toList()); })
WidgetsBinding.instance.addPostFrameCallback( .toList());
(_) => scrollToBottom(), WidgetsBinding.instance.addPostFrameCallback(
); (_) => scrollToBottom(),
} );
}); }
});
}
LiveHttp.liveRoomGetDanmakuToken(roomId: roomId).then((v) { LiveHttp.liveRoomGetDanmakuToken(roomId: roomId).then((v) {
if (v['status']) { if (v['status']) {
LiveDanmakuInfo info = v['data']; LiveDanmakuInfo info = v['data'];

View File

@@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:PiliPlus/http/live.dart'; import 'package:PiliPlus/http/live.dart';
import 'package:PiliPlus/pages/live_room/widgets/chat.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/services/service_locator.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
@@ -75,7 +76,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
videoSourceInit(); videoSourceInit();
_futureBuilderFuture = _liveRoomController.queryLiveInfo(); _futureBuilderFuture = _liveRoomController.queryLiveInfo();
plPlayerController.autoEnterFullscreen(); plPlayerController.autoEnterFullscreen();
_liveRoomController.liveMsg(); plPlayerController.addStatusLister(playerListener);
_listener = plPlayerController.isFullScreen.listen((isFullScreen) { _listener = plPlayerController.isFullScreen.listen((isFullScreen) {
if (isFullScreen != _isFullScreen) { if (isFullScreen != _isFullScreen) {
_isFullScreen = isFullScreen; _isFullScreen = isFullScreen;
@@ -84,6 +85,16 @@ class _LiveRoomPageState extends State<LiveRoomPage>
}); });
} }
void playerListener(PlayerStatus? status) {
if (status != PlayerStatus.playing) {
plPlayerController.danmakuController?.pause();
_liveRoomController.msgStream?.close();
} else {
plPlayerController.danmakuController?.resume();
_liveRoomController.liveMsg();
}
}
void _updateFontSize() async { void _updateFontSize() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
_isPipMode = _isPipMode =
@@ -119,6 +130,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
_liveRoomController.msgStream?.close(); _liveRoomController.msgStream?.close();
// floating?.dispose(); // floating?.dispose();
_node.dispose(); _node.dispose();
plPlayerController.removeStatusLister(playerListener);
plPlayerController.dispose(); plPlayerController.dispose();
_ctr.dispose(); _ctr.dispose();
super.dispose(); super.dispose();
@@ -160,10 +172,13 @@ class _LiveRoomPageState extends State<LiveRoomPage>
fill: fill, fill: fill,
alignment: alignment, alignment: alignment,
plPlayerController: plPlayerController, plPlayerController: plPlayerController,
headerControl: LiveHeaderControl(
plPlayerController: plPlayerController,
floating: floating,
),
bottomControl: BottomControl( bottomControl: BottomControl(
plPlayerController: plPlayerController, plPlayerController: plPlayerController,
liveRoomCtr: _liveRoomController, liveRoomCtr: _liveRoomController,
floating: floating,
onRefresh: () { onRefresh: () {
_futureBuilderFuture = _liveRoomController.queryLiveInfo(); _futureBuilderFuture = _liveRoomController.queryLiveInfo();
}, },

View File

@@ -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:PiliPlus/utils/storage.dart';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:PiliPlus/pages/live_room/index.dart'; import 'package:PiliPlus/pages/live_room/index.dart';
import 'package:PiliPlus/plugin/pl_player/index.dart'; import 'package:PiliPlus/plugin/pl_player/index.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
class BottomControl extends StatelessWidget implements PreferredSizeWidget { class BottomControl extends StatelessWidget implements PreferredSizeWidget {
const BottomControl({ const BottomControl({
required this.plPlayerController, required this.plPlayerController,
required this.liveRoomCtr, required this.liveRoomCtr,
this.floating,
required this.onRefresh, required this.onRefresh,
super.key, super.key,
}); });
final PlPlayerController plPlayerController; final PlPlayerController plPlayerController;
final LiveRoomController liveRoomCtr; final LiveRoomController liveRoomCtr;
final Floating? floating;
final VoidCallback onRefresh; final VoidCallback onRefresh;
final TextStyle subTitleStyle = const TextStyle(fontSize: 12); final TextStyle subTitleStyle = const TextStyle(fontSize: 12);
@@ -39,6 +34,10 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
titleSpacing: 14, titleSpacing: 14,
title: Row( title: Row(
children: [ children: [
PlayOrPauseButton(
plPlayerController: plPlayerController,
),
const SizedBox(width: 10),
ComBtn( ComBtn(
icon: const Icon( icon: const Icon(
Icons.refresh, Icons.refresh,
@@ -78,18 +77,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
// ), // ),
// ), // ),
// const SizedBox(width: 4), // const SizedBox(width: 4),
Obx(
() => IconButton(
onPressed: plPlayerController.setOnlyPlayAudio,
icon: Icon(
size: 18,
plPlayerController.onlyPlayAudio.value
? MdiIcons.musicCircle
: MdiIcons.musicCircleOutline,
color: Colors.white,
),
),
),
Obx( Obx(
() => IconButton( () => IconButton(
onPressed: () { onPressed: () {
@@ -140,32 +128,6 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
), ),
), ),
const SizedBox(width: 10), 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( Obx(
() => SizedBox( () => SizedBox(
width: 30, width: 30,

View File

@@ -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,
),
),
],
),
);
}
}

View File

@@ -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/index.dart';
import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/services/shutdown_timer_service.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../../models/video/play/CDN.dart'; import '../../../../models/video/play/CDN.dart';
import '../../../setting/widgets/select_dialog.dart'; import '../../../setting/widgets/select_dialog.dart';
@@ -179,7 +178,8 @@ class HeaderControlState extends State<HeaderControl> {
), ),
ListTile( ListTile(
dense: true, dense: true,
onTap: () => {Get.back(), scheduleExit()}, onTap: () =>
{Get.back(), Utils.scheduleExit(context, isFullScreen)},
leading: const Icon(Icons.hourglass_top_outlined, size: 20), leading: const Icon(Icons.hourglass_top_outlined, size: 20),
title: const Text('定时关闭', style: titleStyle), title: const Text('定时关闭', style: titleStyle),
), ),
@@ -592,190 +592,6 @@ class HeaderControlState extends State<HeaderControl> {
); );
} }
/// 定时关闭
void scheduleExit() {
const List<int> 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<Icon?>(
(Set<WidgetState> 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() { void showSetVideoQa() {
if (videoInfo.dash == null) { if (videoInfo.dash == null) {

View File

@@ -68,7 +68,7 @@ class PlayOrPauseButtonState extends State<PlayOrPauseButton>
return SizedBox( return SizedBox(
width: 42, width: 42,
height: 34, height: 34,
child: InkWell( child: GestureDetector(
onTap: () async { onTap: () async {
if (player.state.completed) { if (player.state.completed) {
await player.seek(Duration.zero); await player.seek(Duration.zero);

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
@@ -156,12 +157,15 @@ class LiveMessageStream {
int roomId, uid; int roomId, uid;
List<String> servers; List<String> servers;
List<void Function(dynamic obj)> eventListeners = []; List<void Function(dynamic obj)> eventListeners = [];
LiveMessageStream( LiveMessageStream({
{required this.streamToken, required this.streamToken,
required this.roomId, required this.roomId,
required this.uid, required this.uid,
required this.servers}); required this.servers,
});
WebSocket? socket; WebSocket? socket;
StreamSubscription? _socketSubscription;
bool heartBeat = true; bool heartBeat = true;
PiliLogger logger = getLogger(); PiliLogger logger = getLogger();
final String logTag = "LiveStreamService"; final String logTag = "LiveStreamService";
@@ -202,38 +206,39 @@ class LiveMessageStream {
socket = await getSocket(); socket = await getSocket();
// logger.d('$logTag ===> TCP连接建立'); // logger.d('$logTag ===> TCP连接建立');
socket?.add(authPackage.marshal());
// logger.d('$logTag ===> 发送认证包'); // logger.d('$logTag ===> 发送认证包');
await for (var data in socket!) { _socketSubscription = socket?.listen(
PackageHeader? header = PackageHeader.fromBytesData(data); (data) {
if (header != null) { PackageHeader? header = PackageHeader.fromBytesData(data);
List<int> decompressedData = []; if (header != null) {
//心跳包回复不用处理 List<int> decompressedData = [];
if (header.operationCode == 3) continue; //心跳包回复不用处理
if (header.operationCode == 8) { if (header.operationCode == 3) return;
_heartBeat(); if (header.operationCode == 8) {
} _heartBeat();
try { }
switch (header.protocolVer) { try {
case 0: switch (header.protocolVer) {
case 1: case 0:
_processingData(data); case 1:
continue; _processingData(data);
case 2: return;
decompressedData = ZLibDecoder().convert(data.sublist(0x10)); case 2:
break; decompressedData = ZLibDecoder().convert(data.sublist(0x10));
case 3: break;
decompressedData = case 3:
const BrotliDecoder().convert(data.sublist(0x10)); decompressedData =
//debugPrint('Body: ${utf8.decode()}'); 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) { } catch (e) {
SmartDialog.showToast("弹幕地址链接失败"); SmartDialog.showToast("弹幕地址链接失败");
// logger.i('$logTag ===> TCP连接失败: $e'); // logger.i('$logTag ===> TCP连接失败: $e');
@@ -266,12 +271,14 @@ class LiveMessageStream {
await Future.delayed(const Duration(seconds: 30)); await Future.delayed(const Duration(seconds: 30));
//发送心跳包 //发送心跳包
var package = HeartbeatPackage( var package = HeartbeatPackage(
header: PackageHeader( header: PackageHeader(
totalSize: 0, totalSize: 0,
headerSize: 0, headerSize: 0,
protocolVer: 1, protocolVer: 1,
operationCode: 2, operationCode: 2,
seq: heartBeatCount)); seq: heartBeatCount,
),
);
try { try {
socket?.add(package.marshal()); socket?.add(package.marshal());
} catch (_) {} } catch (_) {}
@@ -283,8 +290,10 @@ class LiveMessageStream {
eventListeners.add(func); eventListeners.add(func);
} }
void close() { void close() async {
socket?.close();
heartBeat = false; heartBeat = false;
eventListeners.clear();
_socketSubscription?.cancel();
socket?.close();
} }
} }

View File

@@ -26,6 +26,8 @@ import 'package:PiliPlus/pages/dynamics/tab/controller.dart';
import 'package:PiliPlus/pages/later/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/fav_panel.dart';
import 'package:PiliPlus/pages/video/detail/introduction/widgets/group_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/accounts/account.dart';
import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
@@ -60,6 +62,196 @@ class Utils {
static final _numRegExp = RegExp(r'([\d\.]+)([千万亿])?'); static final _numRegExp = RegExp(r'([\d\.]+)([千万亿])?');
/// 定时关闭
static void scheduleExit(context, isFullScreen, [bool isLive = false]) {
const List<int> 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<Icon?>(
(Set<WidgetState> 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) { static void enterPip(Floating floating, int width, int height) {
Rational aspectRatio = Rational(width, height); Rational aspectRatio = Rational(width, height);
floating.enable( floating.enable(