* opt: play status

* opt: comment
This commit is contained in:
My-Responsitories
2025-10-25 14:45:19 +08:00
committed by GitHub
parent 08944241bb
commit ccb61415f5
9 changed files with 133 additions and 260 deletions

View File

@@ -92,7 +92,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
return;
}
if (playerController.playerStatus.status.value != PlayerStatus.playing) {
if (!playerController.playerStatus.playing) {
return;
}

View File

@@ -165,7 +165,7 @@ class _MainAppState extends State<MainApp>
void _onHideWindow() {
if (_mainController.pauseOnMinimize) {
_mainController.isPlaying =
PlPlayerController.instance?.playerStatus.status.value ==
PlPlayerController.instance?.playerStatus.value ==
PlayerStatus.playing;
PlPlayerController.pauseIfExists();
}

View File

@@ -993,8 +993,7 @@ class VideoDetailController extends GetxController
SmartDialog.showToast('UP主已关闭弹幕');
return;
}
bool isPlaying =
plPlayerController.playerStatus.status.value == PlayerStatus.playing;
final isPlaying = plPlayerController.playerStatus.playing;
if (isPlaying) {
await plPlayerController.pause();
}
@@ -1580,8 +1579,7 @@ class VideoDetailController extends GetxController
void makeHeartBeat() {
if (plPlayerController.enableHeart &&
plPlayerController.playerStatus.status.value !=
PlayerStatus.completed &&
!plPlayerController.playerStatus.completed &&
playedTime != null) {
try {
plPlayerController.makeHeartBeat(

View File

@@ -2,12 +2,14 @@ import 'package:PiliPlus/common/skeleton/video_reply.dart';
import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/view_safe_area.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo, Mode;
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/pages/video/reply_reply/controller.dart';
import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:PiliPlus/utils/num_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
@@ -44,6 +46,51 @@ class VideoReplyReplyPanel extends CommonSlidePage {
@override
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
static Future<void>? toReply(
int oid,
int rootId,
String? rpIdStr,
int type,
Uri? uri,
) {
final rpId = rpIdStr == null ? null : int.tryParse(rpIdStr);
return Get.to(
arguments: {
'oid': oid,
'rpid': rootId,
'id': ?rpId,
'type': type,
'enterUri': ?uri, // save panel
},
() => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('评论详情'),
actions: [
IconButton(
tooltip: '前往',
onPressed: uri == null
? null
: () => PiliScheme.routePush(uri, businessId: type),
icon: const Icon(Icons.open_in_new),
),
],
),
body: ViewSafeArea(
child: VideoReplyReplyPanel(
enableSlide: false,
oid: oid,
rpid: rootId,
isVideoDetail: false,
replyType: type,
firstFloor: null,
id: rpId,
),
),
),
);
}
}
class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>

View File

@@ -379,7 +379,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
introController.canelTimer();
videoDetailController
..playerStatus = plPlayerController?.playerStatus.status.value
..playerStatus = plPlayerController?.playerStatus.value
..brightness = plPlayerController?.brightness.value;
if (plPlayerController != null) {
videoDetailController.makeHeartBeat();
@@ -406,8 +406,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
WidgetsBinding.instance.addObserver(this);
plPlayerController?.isLive = false;
if (videoDetailController.plPlayerController.playerStatus.status.value ==
PlayerStatus.playing &&
if (videoDetailController.plPlayerController.playerStatus.playing &&
videoDetailController.playerStatus != PlayerStatus.playing) {
videoDetailController.plPlayerController.pause();
}
@@ -608,7 +607,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
videoDetailController.isCollapsing
? animHeight
: videoDetailController.isCollapsing ||
plPlayerController?.playerStatus.status.value ==
plPlayerController?.playerStatus.value ==
PlayerStatus.playing
? videoDetailController.minVideoHeight
: kToolbarHeight;
@@ -724,7 +723,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
Text(
'${videoDetailController.playedTime == null
? '立即'
: plPlayerController!.playerStatus.status.value == PlayerStatus.completed
: plPlayerController!.playerStatus.completed
? '重新'
: '继续'}播放',
style: TextStyle(

View File

@@ -69,7 +69,7 @@ class PlPlayerController {
StreamSubscription? _playerEventSubs;
/// [playerStatus] has a [status] observable
final PlPlayerStatus playerStatus = PlPlayerStatus();
final playerStatus = PlPlayerStatus(PlayerStatus.playing);
///
final PlPlayerDataStatus dataStatus = PlPlayerDataStatus();
@@ -154,7 +154,7 @@ class PlPlayerController {
Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream;
/// 播放状态监听
Stream<PlayerStatus> get onPlayerStatusChanged => playerStatus.status.stream;
Stream<PlayerStatus> get onPlayerStatusChanged => playerStatus.stream;
/// 视频时长
Rx<Duration> get duration => _duration;
@@ -551,14 +551,14 @@ class PlPlayerController {
// try to get PlayerStatus
static PlayerStatus? getPlayerStatusIfExists() {
return _instance?.playerStatus.status.value;
return _instance?.playerStatus.value;
}
static Future<void> pauseIfExists({
bool notify = true,
bool isInterrupt = false,
}) async {
if (_instance?.playerStatus.status.value == PlayerStatus.playing) {
if (_instance?.playerStatus.value == PlayerStatus.playing) {
await _instance?.pause(notify: notify, isInterrupt: isInterrupt);
}
}
@@ -1046,13 +1046,13 @@ class PlPlayerController {
disableAutoEnterPip();
}
}
playerStatus.status.value = PlayerStatus.playing;
playerStatus.value = PlayerStatus.playing;
} else {
disableAutoEnterPip();
playerStatus.status.value = PlayerStatus.paused;
playerStatus.value = PlayerStatus.paused;
}
videoPlayerServiceHandler?.onStatusChange(
playerStatus.status.value,
playerStatus.value,
isBuffering.value,
isLive,
);
@@ -1067,14 +1067,14 @@ class PlPlayerController {
}),
videoPlayerController!.stream.completed.listen((event) {
if (event) {
playerStatus.status.value = PlayerStatus.completed;
playerStatus.value = PlayerStatus.completed;
/// 触发回调事件
for (var element in _statusListeners) {
element(PlayerStatus.completed);
}
} else {
// playerStatus.status.value = PlayerStatus.playing;
// playerStatus.value = PlayerStatus.playing;
}
makeHeartBeat(positionSeconds.value, type: HeartBeatType.completed);
}),
@@ -1102,7 +1102,7 @@ class PlPlayerController {
videoPlayerController!.stream.buffering.listen((bool event) {
isBuffering.value = event;
videoPlayerServiceHandler?.onStatusChange(
playerStatus.status.value,
playerStatus.value,
event,
isLive,
);
@@ -1237,7 +1237,7 @@ class PlPlayerController {
} catch (e) {
if (kDebugMode) debugPrint('seek failed: $e');
}
// if (playerStatus.status.value == PlayerStatus.paused) {
// if (playerStatus.value == PlayerStatus.paused) {
// play();
// }
t.cancel();
@@ -1294,14 +1294,14 @@ class PlPlayerController {
audioSessionHandler?.setActive(true);
playerStatus.status.value = PlayerStatus.playing;
playerStatus.value = PlayerStatus.playing;
// screenManager.setOverlays(false);
}
/// 暂停播放
Future<void> pause({bool notify = true, bool isInterrupt = false}) async {
await _videoPlayerController?.pause();
playerStatus.status.value = PlayerStatus.paused;
playerStatus.value = PlayerStatus.paused;
// 主动暂停时让出音频焦点
if (!isInterrupt) {
@@ -1465,7 +1465,7 @@ class PlPlayerController {
return;
}
if (val) {
if (playerStatus.status.value == PlayerStatus.playing) {
if (playerStatus.value == PlayerStatus.playing) {
_longPressStatus.value = val;
HapticFeedback.lightImpact();
await setPlaybackSpeed(
@@ -1655,13 +1655,13 @@ class PlPlayerController {
}
if (!enableHeart || MineController.anonymity.value || progress == 0) {
return;
} else if (playerStatus.status.value == PlayerStatus.paused) {
} else if (playerStatus.value == PlayerStatus.paused) {
if (!isManual) {
return;
}
}
bool isComplete =
playerStatus.status.value == PlayerStatus.completed ||
playerStatus.value == PlayerStatus.completed ||
type == HeartBeatType.completed;
if ((durationSeconds.value - position.value).inMilliseconds > 1000) {
isComplete = false;
@@ -1761,7 +1761,7 @@ class PlPlayerController {
// _showControls.close();
// _controlsLock.close();
// playerStatus.status.close();
// playerStatus.close();
// dataStatus.status.close();
await removeListeners();

View File

@@ -2,18 +2,18 @@ import 'package:get/get.dart';
enum PlayerStatus { completed, playing, paused }
class PlPlayerStatus {
Rx<PlayerStatus> status = Rx(PlayerStatus.paused);
typedef PlPlayerStatus = Rx<PlayerStatus>;
extension PlPlayerStatusExt on PlPlayerStatus {
bool get playing {
return status.value == PlayerStatus.playing;
return value == PlayerStatus.playing;
}
bool get paused {
return status.value == PlayerStatus.paused;
return value == PlayerStatus.paused;
}
bool get completed {
return status.value == PlayerStatus.completed;
return value == PlayerStatus.completed;
}
}

View File

@@ -38,6 +38,7 @@ import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart';
import 'package:PiliPlus/plugin/pl_player/models/double_tap_type.dart';
import 'package:PiliPlus/plugin/pl_player/models/fullscreen_mode.dart';
import 'package:PiliPlus/plugin/pl_player/models/gesture_type.dart';
import 'package:PiliPlus/plugin/pl_player/models/play_status.dart';
import 'package:PiliPlus/plugin/pl_player/models/video_fit_type.dart';
import 'package:PiliPlus/plugin/pl_player/widgets/app_bar_ani.dart';
import 'package:PiliPlus/plugin/pl_player/widgets/backward_seek.dart';

View File

@@ -23,7 +23,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
abstract class PiliScheme {
abstract final class PiliScheme {
static late AppLinks appLinks;
static StreamSubscription? listener;
static final uriDigitRegExp = RegExp(r'/(\d+)');
@@ -123,50 +123,13 @@ abstract class PiliScheme {
// to video reply
String? oid = uriDigitRegExp.firstMatch(path)?.group(1);
int? rpid = int.tryParse(queryParameters['comment_root_id']!);
String? commentSecondaryId =
queryParameters['comment_secondary_id'];
if (oid != null && rpid != null) {
Get.to(
arguments: {
'oid': oid,
'rpid': rpid,
'type': 1,
'id': commentSecondaryId,
},
() => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('评论详情'),
actions: [
IconButton(
tooltip: '前往原视频',
onPressed: () {
routePush(
Uri(
scheme: uri.scheme,
host: uri.host,
path: uri.path,
),
);
},
icon: const Icon(Icons.open_in_new),
),
],
),
body: ViewSafeArea(
child: VideoReplyReplyPanel(
enableSlide: false,
oid: int.parse(oid),
rpid: rpid,
isVideoDetail: false,
replyType: 1,
firstFloor: null,
id: commentSecondaryId != null
? int.tryParse(commentSecondaryId)
: null,
),
),
),
VideoReplyReplyPanel.toReply(
int.parse(oid),
rpid,
queryParameters['comment_secondary_id'],
1,
uri.replace(query: ''),
);
return true;
}
@@ -248,119 +211,36 @@ abstract class PiliScheme {
}
return false;
case 'comment':
if (path.startsWith("/detail/")) {
if (path.startsWith("/detail/") || path.startsWith("/msg_fold/")) {
// bilibili://comment/detail/17/832703053858603029/238686570016/?subType=0&anchor=238686628816&showEnter=1&extraIntentId=0&scene=1&enterName=%E6%9F%A5%E7%9C%8B%E5%8A%A8%E6%80%81%E8%AF%A6%E6%83%85&enterUri=bilibili://following/detail/832703053858603029
// bilibili://comment/msg_fold/1/22222/33333/11111/?enterUri=bilibili://video/22222 //(aid)
// bilibili://comment/msg_fold/11/22222/33333/11111/?enterUri=bilibili://following/detail/44444 (dynId)
final pathSegments = uri.pathSegments;
final queryParameters = uri.queryParameters;
final type = int.parse(pathSegments[1]); // business_id
final oid = int.parse(pathSegments[2]); // subject_id
final rootId = int.parse(pathSegments[3]); // root_id // target_id
final rpId =
queryParameters['anchor'] !=
null // source_id
? int.tryParse(queryParameters['anchor']!)
: null;
// int subType = int.parse(queryParameters['subType'] ?? '0');
// int extraIntentId =
// int.parse(queryParameters['extraIntentId'] ?? '0');
final enterUri = queryParameters['enterUri'];
Get.to(
arguments: {
'oid': oid,
'rpid': rootId,
'id': rpId,
'type': type,
'enterUri': enterUri,
},
() => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('评论详情'),
actions:
enterUri != null || const [11, 16, 17].contains(type)
? [
IconButton(
tooltip: '前往',
onPressed: () {
if (enterUri != null) {
routePush(Uri.parse(enterUri));
} else {
routePush(
Uri.parse(
'bilibili://following/detail/$oid',
),
);
}
},
icon: const Icon(Icons.open_in_new),
),
]
: null,
),
body: ViewSafeArea(
child: VideoReplyReplyPanel(
enableSlide: false,
oid: oid,
rpid: rootId,
id: rpId,
isVideoDetail: false,
replyType: type,
firstFloor: null,
),
),
),
);
return true;
} else if (path.startsWith("/msg_fold/")) {
// bilibili://comment/msg_fold/1/22222/33333/11111/?enterUri=bilibili://video/22222 //(aid)
// bilibili://comment/msg_fold/11/22222/33333/11111/?enterUri=bilibili://following/detail/44444 (dynId)
List<String> pathSegments = uri.pathSegments;
int type = int.parse(pathSegments[1]); // business_id
int oid = int.parse(pathSegments[2]); // subject_id
int rpId = int.parse(pathSegments[3]); // source_id
Get.to(
arguments: {
'oid': oid,
'rpid': rpId,
'type': type,
},
() => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('评论详情'),
actions: [
IconButton(
tooltip: '前往',
onPressed: () {
String? enterUri = uri.queryParameters['enterUri'];
if (enterUri != null) {
routePush(Uri.parse(enterUri), businessId: type);
} else {
routePush(
Uri.parse('bilibili://following/detail/$oid'),
businessId: type,
);
}
},
icon: const Icon(Icons.open_in_new),
),
],
),
body: ViewSafeArea(
child: VideoReplyReplyPanel(
enableSlide: false,
oid: oid,
rpid: rpId,
isVideoDetail: false,
replyType: type,
firstFloor: null,
),
),
),
VideoReplyReplyPanel.toReply(
oid,
rootId,
queryParameters['anchor'], // source_id
type,
enterUri != null
? Uri.parse(enterUri)
: const [11, 16, 17].contains(type)
? Uri(
scheme: 'bilibili',
host: 'following',
path: 'detail/$oid',
)
: null,
);
return true;
}
return false;
case 'following':
// businessId == 17 => dynId == oid
@@ -388,53 +268,19 @@ abstract class PiliScheme {
if (commentRootId != null) {
String? dynId = uriDigitRegExp.firstMatch(path)?.group(1);
int? rpid = int.tryParse(commentRootId);
final commentSecondaryId =
queryParameters['comment_secondary_id'];
if (dynId != null && rpid != null) {
Get.to(
arguments: {
'oid': oid ?? dynId,
'rpid': rpid,
'type': businessId ?? 17,
'id': commentSecondaryId,
},
() => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('评论详情'),
actions: [
IconButton(
tooltip: '前往',
onPressed: () => _onPushDynDetail(uri, off),
icon: const Icon(Icons.open_in_new),
),
],
),
body: ViewSafeArea(
child: VideoReplyReplyPanel(
enableSlide: false,
oid: oid ?? int.parse(dynId),
rpid: rpid,
isVideoDetail: false,
replyType: businessId ?? 17,
firstFloor: null,
id: commentSecondaryId != null
? int.tryParse(commentSecondaryId)
: null,
),
),
),
VideoReplyReplyPanel.toReply(
oid ?? int.parse(dynId),
rpid,
queryParameters['comment_secondary_id'],
businessId ?? 17,
uri.replace(query: ''),
);
return true;
}
return true;
} else {
bool hasMatch = _onPushDynDetail(uri, off);
return hasMatch;
}
} else {
bool hasMatch = _onPushDynDetail(uri, off);
return hasMatch;
}
return _onPushDynDetail(uri, off);
case 'album':
String? rid = uriDigitRegExp.firstMatch(path)?.group(1);
if (rid != null) {
@@ -797,6 +643,20 @@ abstract class PiliScheme {
final res = IdUtils.matchAvorBv(input: path);
if (res.isNotEmpty) {
final queryParameters = uri.queryParameters;
final rootIdStr = queryParameters['comment_root_id'];
if (rootIdStr != null) {
VideoReplyReplyPanel.toReply(
res.av ?? IdUtils.bv2av(res.bv!),
int.parse(rootIdStr),
queryParameters['comment_secondary_id'],
1,
uri.replace(
queryParameters: Map.of(queryParameters)
..remove('comment_root_id'),
),
);
return true;
}
videoPush(
res.av,
res.bv,
@@ -885,48 +745,16 @@ abstract class PiliScheme {
case 'comment':
// https://www.bilibili.com/h5/comment/sub?oid=123456&pageType=1&root=87654321
final queryParameters = uri.queryParameters;
String? oid = queryParameters['oid'];
String? root = queryParameters['root'];
String? pageType = queryParameters['pageType'];
final oid = queryParameters['oid'];
final root = queryParameters['root'];
final pageType = queryParameters['pageType'];
if (oid != null && root != null && pageType != null) {
String? commentSecondaryId = queryParameters['comment_secondary_id'];
Get.to(
arguments: {
'oid': oid,
'rpid': root,
'type': pageType,
'id': commentSecondaryId,
},
() => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('评论详情'),
actions: pageType == '1'
? [
IconButton(
tooltip: '前往',
onPressed: () {
videoPush(int.parse(oid), null);
},
icon: const Icon(Icons.open_in_new),
),
]
: null,
),
body: ViewSafeArea(
child: VideoReplyReplyPanel(
enableSlide: false,
oid: int.parse(oid),
rpid: int.parse(root),
isVideoDetail: false,
replyType: int.parse(pageType),
firstFloor: null,
id: commentSecondaryId != null
? int.tryParse(commentSecondaryId)
: null,
),
),
),
VideoReplyReplyPanel.toReply(
int.parse(oid),
int.parse(root),
queryParameters['comment_secondary_id'],
int.parse(pageType),
Uri(scheme: 'bilibili', host: 'video', path: oid),
);
return true;
}