mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-12 13:07:42 +08:00
mod: live schedule
Closes #581 Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -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'];
|
||||||
|
|||||||
@@ -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();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
89
lib/pages/live_room/widgets/header_control.dart
Normal file
89
lib/pages/live_room/widgets/header_control.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user