diff --git a/README.md b/README.md
index fac2a885..6d2c1d56 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,14 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
```
+
+
+
+## 技术交流
+
+Telegram: https://t.me/+lm_oOVmF0RJiODk1
+
+
## 功能
@@ -100,6 +108,7 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
- [x] 主题模式:亮色/暗色/跟随系统
- [x] 震动反馈(可选)
- [x] 高帧率
+ - [x] 自动全屏
- [ ] 等等
@@ -117,11 +126,6 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
感谢使用
-
-
-## 技术交流
-
-Telegram https://t.me/+lm_oOVmF0RJiODk1
diff --git a/lib/http/api.dart b/lib/http/api.dart
index b32c7e40..e1e011eb 100644
--- a/lib/http/api.dart
+++ b/lib/http/api.dart
@@ -292,4 +292,6 @@ class Api {
// 多少人在看
// https://api.bilibili.com/x/player/online/total?aid=913663681&cid=1203559746&bvid=BV1MM4y1s7NZ&ts=56427838
static const String onlineTotal = '/x/player/online/total';
+
+ static const String webDanmaku = '/x/v2/dm/web/seg.so';
}
diff --git a/lib/http/danmaku.dart b/lib/http/danmaku.dart
new file mode 100644
index 00000000..020c89ea
--- /dev/null
+++ b/lib/http/danmaku.dart
@@ -0,0 +1,33 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+import 'package:pilipala/http/index.dart';
+import 'package:pilipala/models/danmaku/dm.pb.dart';
+
+import 'constants.dart';
+
+class DanmakaHttp {
+ // 获取视频弹幕
+ static Future queryDanmaku({
+ required int cid,
+ required int segmentIndex,
+ }) async {
+ // 构建参数对象
+ Map params = {
+ 'type': 1,
+ 'oid': cid,
+ 'segment_index': segmentIndex,
+ };
+
+ // 计算函数
+ Future computeTask(Map params) async {
+ var response = await Request().get(
+ Api.webDanmaku,
+ data: params,
+ extra: {'resType': ResponseType.bytes},
+ );
+ return DmSegMobileReply.fromBuffer(response.data);
+ }
+
+ return await compute(computeTask, params);
+ }
+}
diff --git a/lib/http/init.dart b/lib/http/init.dart
index 5f9e1e2b..1e821062 100644
--- a/lib/http/init.dart
+++ b/lib/http/init.dart
@@ -41,6 +41,7 @@ class Request {
log("setCookie, ${e.toString()}");
}
}
+ setOptionsHeaders(userInfo);
}
if (cookie.isEmpty) {
@@ -69,6 +70,15 @@ class Request {
return token;
}
+ static setOptionsHeaders(userInfo) {
+ dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
+ dio.options.headers['env'] = 'prod';
+ dio.options.headers['app-key'] = 'android64';
+ dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
+ dio.options.headers['x-bili-aurora-zone'] = 'sh001';
+ dio.options.headers['referer'] = 'https://www.bilibili.com/';
+ }
+
/*
* config it and create
*/
@@ -87,17 +97,6 @@ class Request {
},
);
- Box userInfoCache = GStrorage.userInfo;
- var userInfo = userInfoCache.get('userInfoCache');
- if (userInfo != null && userInfo.mid != null) {
- options.headers['x-bili-mid'] = userInfo.mid.toString();
- options.headers['env'] = 'prod';
- options.headers['app-key'] = 'android64';
- options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
- options.headers['x-bili-aurora-zone'] = 'sh001';
- options.headers['referer'] = 'https://www.bilibili.com/';
- }
-
dio = Dio(options)
..httpClientAdapter = Http2Adapter(
ConnectionManager(
diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart
index 9a86fbb9..4b9e8770 100644
--- a/lib/http/interceptor.dart
+++ b/lib/http/interceptor.dart
@@ -17,8 +17,6 @@ class ApiInterceptor extends Interceptor {
handler.next(options);
}
- Box localCache = GStrorage.localCache;
-
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
try {
@@ -29,8 +27,11 @@ class ApiInterceptor extends Interceptor {
final uri = Uri.parse(locations.first);
final accessKey = uri.queryParameters['access_key'];
final mid = uri.queryParameters['mid'];
- localCache
- .put(LocalCacheKey.accessKey, {'mid': mid, 'value': accessKey});
+ try {
+ Box localCache = GStrorage.localCache;
+ localCache.put(
+ LocalCacheKey.accessKey, {'mid': mid, 'value': accessKey});
+ } catch (_) {}
}
}
}
diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart
index e63e797d..c027f8af 100644
--- a/lib/pages/bangumi/introduction/controller.dart
+++ b/lib/pages/bangumi/introduction/controller.dart
@@ -258,6 +258,7 @@ class BangumiIntroController extends GetxController {
Get.find(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid;
videoDetailCtr.cid = cid;
+ videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl();
// 重新请求评论
try {
diff --git a/lib/pages/danmaku/controller.dart b/lib/pages/danmaku/controller.dart
new file mode 100644
index 00000000..ebe7712d
--- /dev/null
+++ b/lib/pages/danmaku/controller.dart
@@ -0,0 +1,64 @@
+import 'package:pilipala/http/danmaku.dart';
+import 'package:pilipala/models/danmaku/dm.pb.dart';
+import 'package:pilipala/plugin/pl_player/index.dart';
+
+class PlDanmakuController {
+ PlDanmakuController(this.cid, this.playerController);
+ final int cid;
+ final PlPlayerController playerController;
+ late Duration videoDuration;
+ // 按 6min 分段
+ int segCount = 0;
+ List dmSegList = [];
+ int currentSegIndex = 0;
+ int currentDmIndex = 0;
+
+ void calcSegment() {
+ segCount = (videoDuration.inSeconds / (60 * 6)).ceil();
+ }
+
+ Future> queryDanmaku() async {
+ dmSegList.clear();
+ for (int segIndex = 1; segIndex <= segCount; segIndex++) {
+ DmSegMobileReply result =
+ await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: segIndex);
+ if (result.elems.isNotEmpty) {
+ result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
+ dmSegList.add(result);
+ }
+ }
+ if (dmSegList.isNotEmpty) {
+ findClosestPositionIndex(playerController.position.value.inMilliseconds);
+ }
+ return dmSegList;
+ }
+
+ /// 查询当前最接近的弹幕
+ void findClosestPositionIndex(int position) {
+ int segIndex = (position / (6 * 60 * 1000)).ceil() - 1;
+ if (segIndex < 0) segIndex = 0;
+ List elems = dmSegList[segIndex].elems;
+
+ if (segIndex < dmSegList.length) {
+ int left = 0;
+ int right = elems.length;
+
+ while (left < right) {
+ int mid = (right + left) ~/ 2;
+ var midPosition = elems[mid].progress;
+
+ if (midPosition >= position) {
+ right = mid;
+ } else {
+ left = mid + 1;
+ }
+ }
+
+ currentSegIndex = segIndex;
+ currentDmIndex = right;
+ } else {
+ currentSegIndex = segIndex;
+ currentDmIndex = 0;
+ }
+ }
+}
diff --git a/lib/pages/danmaku/index.dart b/lib/pages/danmaku/index.dart
new file mode 100644
index 00000000..004ee0c3
--- /dev/null
+++ b/lib/pages/danmaku/index.dart
@@ -0,0 +1,4 @@
+library pldanmaku;
+
+export './controller.dart';
+export 'view.dart';
diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart
new file mode 100644
index 00000000..feaf60b9
--- /dev/null
+++ b/lib/pages/danmaku/view.dart
@@ -0,0 +1,127 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:ns_danmaku/ns_danmaku.dart';
+import 'package:pilipala/pages/danmaku/index.dart';
+import 'package:pilipala/plugin/pl_player/index.dart';
+import 'package:pilipala/utils/danmaku.dart';
+
+/// 传入播放器控制器,监听播放进度,加载对应弹幕
+class PlDanmaku extends StatefulWidget {
+ final int cid;
+ final PlPlayerController playerController;
+
+ const PlDanmaku({
+ super.key,
+ required this.cid,
+ required this.playerController,
+ });
+
+ @override
+ State createState() => _PlDanmakuState();
+}
+
+class _PlDanmakuState extends State {
+ late PlPlayerController playerController;
+ late PlDanmakuController _plDanmakuController;
+ DanmakuController? _controller;
+ bool danmuPlayStatus = true;
+
+ @override
+ void initState() {
+ super.initState();
+ _plDanmakuController =
+ PlDanmakuController(widget.cid, widget.playerController);
+ if (mounted) {
+ playerController = widget.playerController;
+ _plDanmakuController.videoDuration = playerController.duration.value;
+ _plDanmakuController
+ ..calcSegment()
+ ..queryDanmaku();
+ playerController
+ ..addStatusLister(playerListener)
+ ..addPositionListener(videoPositionListen);
+ }
+ }
+
+ // 播放器状态监听
+ void playerListener(PlayerStatus? status) {
+ if (status == PlayerStatus.paused) {
+ _controller!.pause();
+ }
+ if (status == PlayerStatus.playing) {
+ _controller!.onResume();
+ }
+ }
+
+ void videoPositionListen(Duration position) {
+ if (!danmuPlayStatus) {
+ _controller!.onResume();
+ danmuPlayStatus = true;
+ }
+ PlDanmakuController ctr = _plDanmakuController;
+ int currentPosition = position.inMilliseconds;
+
+ // 超出分段数返回
+ if (ctr.currentSegIndex >= ctr.dmSegList.length) {
+ return;
+ }
+ if (ctr.dmSegList.isEmpty ||
+ ctr.dmSegList[ctr.currentSegIndex].elems.isEmpty) {
+ return;
+ }
+ // 超出当前分段的弹幕总数返回
+ if (ctr.currentDmIndex >= ctr.dmSegList[ctr.currentSegIndex].elems.length) {
+ ctr.currentDmIndex = 0;
+ ctr.currentSegIndex++;
+ return;
+ }
+ var element = ctr.dmSegList[ctr.currentSegIndex].elems[ctr.currentDmIndex];
+ var delta = currentPosition - element.progress;
+
+ if (delta >= 0 && delta < 200) {
+ _controller!.addItems([
+ DanmakuItem(
+ element.content,
+ color: DmUtils.decimalToColor(element.color),
+ time: element.progress,
+ type: DmUtils.getPosition(element.mode),
+ )
+ ]);
+ ctr.currentDmIndex++;
+ } else {
+ if (!playerController.isOpenDanmu.value) {
+ _controller!.pause();
+ danmuPlayStatus = false;
+ return;
+ }
+ ctr.findClosestPositionIndex(position.inMilliseconds);
+ }
+ }
+
+ @override
+ void dispose() {
+ playerController.removePositionListener(videoPositionListen);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Obx(
+ () => AnimatedOpacity(
+ opacity: playerController.isOpenDanmu.value ? 1 : 0,
+ duration: const Duration(milliseconds: 100),
+ child: DanmakuView(
+ createdController: (DanmakuController e) async {
+ _controller = e;
+ },
+ option: DanmakuOption(
+ fontSize: 15,
+ area: 0.5,
+ duration: 5,
+ ),
+ statusChanged: (isPlaying) {},
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/favDetail/controller.dart b/lib/pages/favDetail/controller.dart
index 5229ee87..8b772716 100644
--- a/lib/pages/favDetail/controller.dart
+++ b/lib/pages/favDetail/controller.dart
@@ -15,6 +15,8 @@ class FavDetailController extends GetxController {
bool isLoadingMore = false;
RxMap favInfo = {}.obs;
RxList favList = [FavDetailItemData()].obs;
+ RxString loadingText = '加载中...'.obs;
+ int mediaCount = 0;
@override
void onInit() {
@@ -27,6 +29,11 @@ class FavDetailController extends GetxController {
}
Future queryUserFavFolderDetail({type = 'init'}) async {
+ if (type == 'onLoad' && favList.length >= mediaCount) {
+ loadingText.value = '没有更多了';
+ return;
+ }
+ isLoadingMore = true;
var res = await await UserHttp.userFavFolderDetail(
pn: currentPage,
ps: 20,
@@ -36,11 +43,16 @@ class FavDetailController extends GetxController {
favInfo.value = res['data'].info;
if (currentPage == 1 && type == 'init') {
favList.value = res['data'].medias;
- } else if (type == 'onload') {
+ mediaCount = res['data'].info['media_count'];
+ } else if (type == 'onLoad') {
favList.addAll(res['data'].medias);
}
+ if (favList.length >= mediaCount) {
+ loadingText.value = '没有更多了';
+ }
}
currentPage += 1;
+ isLoadingMore = false;
return res;
}
@@ -64,6 +76,6 @@ class FavDetailController extends GetxController {
}
onLoad() {
- queryUserFavFolderDetail(type: 'onload');
+ queryUserFavFolderDetail(type: 'onLoad');
}
}
diff --git a/lib/pages/favDetail/view.dart b/lib/pages/favDetail/view.dart
index a99d1c03..426bfa8f 100644
--- a/lib/pages/favDetail/view.dart
+++ b/lib/pages/favDetail/view.dart
@@ -1,9 +1,12 @@
import 'dart:async';
+import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
+import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/pages/favDetail/index.dart';
import 'widget/fav_video_card.dart';
@@ -37,10 +40,9 @@ class _FavDetailPageState extends State {
if (_controller.position.pixels >=
_controller.position.maxScrollExtent - 200) {
- if (!_favDetailController.isLoadingMore) {
- _favDetailController.isLoadingMore = true;
+ EasyThrottle.throttle('favDetail', const Duration(seconds: 1), () {
_favDetailController.onLoad();
- }
+ });
}
},
);
@@ -183,12 +185,7 @@ class _FavDetailPageState extends State {
Map data = snapshot.data;
if (data['status']) {
if (_favDetailController.item!.mediaCount == 0) {
- return const SliverToBoxAdapter(
- child: SizedBox(
- height: 300,
- child: Center(child: Text('没有内容')),
- ),
- );
+ return const NoData();
} else {
return Obx(
() => SliverList(
@@ -207,18 +204,30 @@ class _FavDetailPageState extends State {
);
}
} else {
- return const SliverToBoxAdapter(
- child: SizedBox(
- height: 300,
- child: Center(child: Text('加载中')),
- ),
+ // 骨架屏
+ return SliverList(
+ delegate: SliverChildBuilderDelegate((context, index) {
+ return const VideoCardHSkeleton();
+ }, childCount: 10),
);
}
},
),
SliverToBoxAdapter(
- child: SizedBox(
- height: MediaQuery.of(context).padding.bottom + 20,
+ child: Container(
+ height: MediaQuery.of(context).padding.bottom + 60,
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(context).padding.bottom),
+ child: Center(
+ child: Obx(
+ () => Text(
+ _favDetailController.loadingText.value,
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.outline,
+ fontSize: 13),
+ ),
+ ),
+ ),
),
)
],
diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart
index 8f9d8226..6d74c5b3 100644
--- a/lib/pages/setting/play_setting.dart
+++ b/lib/pages/setting/play_setting.dart
@@ -78,6 +78,18 @@ class _PlaySettingState extends State {
setKey: SettingBoxKey.enableAutoBrightness,
defaultVal: false,
),
+ const SetSwitchItem(
+ title: '自动全屏',
+ subTitle: '视频开始播放时进入全屏',
+ setKey: SettingBoxKey.enableAutoEnter,
+ defaultVal: false,
+ ),
+ const SetSwitchItem(
+ title: '自动退出',
+ subTitle: '视频结束播放时退出全屏',
+ setKey: SettingBoxKey.enableAutoExit,
+ defaultVal: false,
+ ),
ListTile(
dense: false,
title: Text('默认画质', style: titleStyle),
diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart
index 53f0e16d..e3716f18 100644
--- a/lib/pages/video/detail/controller.dart
+++ b/lib/pages/video/detail/controller.dart
@@ -21,6 +21,7 @@ class VideoDetailController extends GetxController
/// 路由传参
String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!);
+ RxInt danmakuCid = 0.obs;
String heroTag = Get.arguments['heroTag'];
// 视频详情
Map videoItem = {};
@@ -73,6 +74,7 @@ class VideoDetailController extends GetxController
// 默认记录历史记录
bool enableHeart = true;
var userInfo;
+ late bool isFirstTime = true;
@override
void onInit() {
@@ -100,6 +102,7 @@ class VideoDetailController extends GetxController
localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false;
}
+ danmakuCid.value = cid;
}
showReplyReplyPanel() {
@@ -193,6 +196,7 @@ class VideoDetailController extends GetxController
bvid: bvid,
cid: cid,
enableHeart: enableHeart,
+ isFirstTime: isFirstTime,
);
}
@@ -233,7 +237,6 @@ class VideoDetailController extends GetxController
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code))!;
- print(currentDecodeFormats.description);
try {
// 当前视频没有对应格式返回第一个
bool flag = false;
@@ -287,6 +290,7 @@ class VideoDetailController extends GetxController
defaultST = Duration(milliseconds: data.lastPlayTime!);
if (autoPlay.value) {
await playerInit();
+ isShowCover.value = false;
}
} else {
if (result['code'] == -404) {
diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart
index ea63c14d..9f756c7d 100644
--- a/lib/pages/video/detail/introduction/controller.dart
+++ b/lib/pages/video/detail/introduction/controller.dart
@@ -418,6 +418,7 @@ class VideoIntroController extends GetxController {
Get.find(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid;
videoDetailCtr.cid = cid;
+ videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl();
// 重新请求评论
try {
diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart
index c993fda9..75eef280 100644
--- a/lib/pages/video/detail/introduction/view.dart
+++ b/lib/pages/video/detail/introduction/view.dart
@@ -31,9 +31,10 @@ class VideoIntroPanel extends StatefulWidget {
class _VideoIntroPanelState extends State
with AutomaticKeepAliveClientMixin {
- final VideoIntroController videoIntroController =
- Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
+ late String heroTag;
+ late VideoIntroController videoIntroController;
VideoDetailData? videoDetail;
+ late Future? _futureBuilderFuture;
// 添加页面缓存
@override
@@ -42,6 +43,11 @@ class _VideoIntroPanelState extends State
@override
void initState() {
super.initState();
+
+ /// fix 全屏时参数丢失
+ heroTag = Get.arguments['heroTag'];
+ videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
+ _futureBuilderFuture = videoIntroController.queryVideoIntro();
videoIntroController.videoDetail.listen((value) {
videoDetail = value;
});
@@ -57,15 +63,20 @@ class _VideoIntroPanelState extends State
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
- future: videoIntroController.queryVideoIntro(),
+ future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
+ if (snapshot.data == null) {
+ return const SliverToBoxAdapter(child: SizedBox());
+ }
if (snapshot.data['status']) {
// 请求成功
return Obx(
() => VideoInfo(
- loadingStatus: false,
- videoDetail: videoIntroController.videoDetail.value),
+ loadingStatus: false,
+ videoDetail: videoIntroController.videoDetail.value,
+ heroTag: heroTag,
+ ),
);
} else {
// 请求错误
@@ -79,7 +90,11 @@ class _VideoIntroPanelState extends State
);
}
} else {
- return VideoInfo(loadingStatus: true, videoDetail: videoDetail);
+ return VideoInfo(
+ loadingStatus: true,
+ videoDetail: videoDetail,
+ heroTag: heroTag,
+ );
}
},
);
@@ -89,8 +104,10 @@ class _VideoIntroPanelState extends State
class VideoInfo extends StatefulWidget {
final bool loadingStatus;
final VideoDetailData? videoDetail;
+ final String? heroTag;
- const VideoInfo({Key? key, this.loadingStatus = false, this.videoDetail})
+ const VideoInfo(
+ {Key? key, this.loadingStatus = false, this.videoDetail, this.heroTag})
: super(key: key);
@override
@@ -98,7 +115,8 @@ class VideoInfo extends StatefulWidget {
}
class _VideoInfoState extends State with TickerProviderStateMixin {
- final String heroTag = Get.arguments['heroTag'];
+ // final String heroTag = Get.arguments['heroTag'];
+ late String heroTag;
late final VideoIntroController videoIntroController;
late final VideoDetailController videoDetailCtr;
late final Map videoItem;
@@ -117,7 +135,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
@override
void initState() {
super.initState();
-
+ heroTag = widget.heroTag!;
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
videoDetailCtr = Get.find(tag: heroTag);
videoItem = videoIntroController.videoItem!;
diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart
index b51510dc..6568a716 100644
--- a/lib/pages/video/detail/view.dart
+++ b/lib/pages/video/detail/view.dart
@@ -10,6 +10,7 @@ import 'package:pilipala/common/widgets/sliver_header.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/bangumi/introduction/index.dart';
+import 'package:pilipala/pages/danmaku/view.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/pages/video/detail/controller.dart';
@@ -41,7 +42,6 @@ class _VideoDetailPageState extends State
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
PlayerStatus playerStatus = PlayerStatus.playing;
- // bool isShowCover = true;
double doubleOffset = 0;
Box localCache = GStrorage.localCache;
@@ -49,11 +49,15 @@ class _VideoDetailPageState extends State
late double statusBarHeight;
final videoHeight = Get.size.width * 9 / 16;
late Future _futureBuilderFuture;
+ // 自动退出全屏
+ late bool autoExitFullcreen;
@override
void initState() {
super.initState();
statusBarHeight = localCache.get('statusBarHeight');
+ autoExitFullcreen =
+ setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
videoSourceInit();
appbarStreamListen();
}
@@ -63,7 +67,7 @@ class _VideoDetailPageState extends State
_futureBuilderFuture = videoDetailController.queryVideoUrl();
if (videoDetailController.autoPlay.value) {
plPlayerController = videoDetailController.plPlayerController;
- playerListener();
+ plPlayerController!.addStatusLister(playerListener);
}
}
@@ -79,23 +83,15 @@ class _VideoDetailPageState extends State
}
// 播放器状态监听
- void playerListener() {
- plPlayerController!.onPlayerStatusChanged.listen(
- (PlayerStatus status) async {
- playerStatus = status;
- if (status == PlayerStatus.playing) {
- videoDetailController.isShowCover.value = false;
- } else {
- // 播放完成停止 or 切换下一个
- if (status == PlayerStatus.completed) {
- // 当只有1p或多p未打开自动播放时,播放完成还原进度条,展示控制栏
- plPlayerController!.seekTo(Duration.zero);
- plPlayerController!.onLockControl(false);
- plPlayerController!.videoPlayerController!.pause();
- }
- }
- },
- );
+ void playerListener(PlayerStatus? status) {
+ if (status == PlayerStatus.completed) {
+ // 结束播放退出全屏
+ if (autoExitFullcreen) {
+ plPlayerController!.triggerFullScreen(status: false);
+ }
+ // 播放完展示控制栏
+ plPlayerController!.onLockControl(false);
+ }
}
// 继续播放或重新播放
@@ -110,11 +106,11 @@ class _VideoDetailPageState extends State
plPlayerController = videoDetailController.plPlayerController;
videoDetailController.autoPlay.value = true;
videoDetailController.isShowCover.value = false;
- playerListener();
}
@override
void dispose() {
+ plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.dispose();
super.dispose();
}
@@ -128,6 +124,7 @@ class _VideoDetailPageState extends State
}
videoDetailController.defaultST = plPlayerController!.position.value;
videoIntroController.isPaused = true;
+ plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause();
super.didPushNext();
}
@@ -135,12 +132,14 @@ class _VideoDetailPageState extends State
@override
// 返回当前页面时
void didPopNext() async {
+ videoDetailController.isFirstTime = false;
videoDetailController.playerInit();
videoIntroController.isPaused = false;
if (_extendNestCtr.position.pixels == 0) {
await Future.delayed(const Duration(milliseconds: 300));
plPlayerController!.play();
}
+ plPlayerController!.addStatusLister(playerListener);
super.didPopNext();
}
@@ -187,120 +186,127 @@ class _VideoDetailPageState extends State
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
- return Hero(
- tag: videoDetailController.heroTag,
- child: Stack(
- children: [
- FutureBuilder(
- future: _futureBuilderFuture,
- builder: ((context, snapshot) {
- if (snapshot.hasData &&
- snapshot.data['status']) {
- return Obx(
- () => videoDetailController
- .autoPlay.value
- ? PLVideoPlayer(
+ return Stack(
+ children: [
+ FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: ((context, snapshot) {
+ if (snapshot.hasData &&
+ snapshot.data['status']) {
+ return Obx(
+ () => !videoDetailController
+ .autoPlay.value
+ ? const SizedBox()
+ : PLVideoPlayer(
+ controller: plPlayerController!,
+ headerControl: HeaderControl(
controller:
- plPlayerController!,
- headerControl: HeaderControl(
- controller:
- plPlayerController,
- videoDetailCtr:
- videoDetailController,
+ plPlayerController,
+ videoDetailCtr:
+ videoDetailController,
+ ),
+ danmuWidget: Obx(
+ () => PlDanmaku(
+ key: Key(
+ videoDetailController
+ .danmakuCid.value
+ .toString()),
+ cid: videoDetailController
+ .danmakuCid.value,
+ playerController:
+ plPlayerController!,
),
- )
- : const SizedBox(),
- );
- } else {
- return const SizedBox();
- }
- }),
- ),
- Obx(
- () => Visibility(
- visible: videoDetailController
- .isShowCover.value,
- child: Positioned(
- top: 0,
- left: 0,
- right: 0,
- child: NetworkImgLayer(
- type: 'emote',
- src: videoDetailController
- .videoItem['pic'],
- width: maxWidth,
- height: maxHeight,
- ),
+ ),
+ ),
+ );
+ } else {
+ return const SizedBox();
+ }
+ }),
+ ),
+
+ Obx(
+ () => Visibility(
+ visible:
+ videoDetailController.isShowCover.value,
+ child: Positioned(
+ top: 0,
+ left: 0,
+ right: 0,
+ child: NetworkImgLayer(
+ type: 'emote',
+ src: videoDetailController
+ .videoItem['pic'],
+ width: maxWidth,
+ height: maxHeight,
),
),
),
+ ),
- /// 关闭自动播放时 手动播放
- Obx(
- () => Visibility(
- visible: videoDetailController
- .isShowCover.value &&
- videoDetailController
- .isEffective.value &&
- !videoDetailController
- .autoPlay.value,
- child: Stack(
- children: [
- Positioned(
- top: 0,
- left: 0,
- right: 0,
- child: AppBar(
- primary: false,
- foregroundColor: Colors.white,
+ /// 关闭自动播放时 手动播放
+ Obx(
+ () => Visibility(
+ visible: videoDetailController
+ .isShowCover.value &&
+ videoDetailController
+ .isEffective.value &&
+ !videoDetailController.autoPlay.value,
+ child: Stack(
+ children: [
+ Positioned(
+ top: 0,
+ left: 0,
+ right: 0,
+ child: AppBar(
+ primary: false,
+ foregroundColor: Colors.white,
+ backgroundColor:
+ Colors.transparent,
+ actions: [
+ IconButton(
+ tooltip: '稍后再看',
+ onPressed: () async {
+ var res = await UserHttp
+ .toViewLater(
+ bvid:
+ videoDetailController
+ .bvid);
+ SmartDialog.showToast(
+ res['msg']);
+ },
+ icon: const Icon(
+ Icons.history_outlined),
+ ),
+ const SizedBox(width: 14)
+ ],
+ ),
+ ),
+ Positioned(
+ right: 12,
+ bottom: 10,
+ child: TextButton.icon(
+ style: ButtonStyle(
backgroundColor:
- Colors.transparent,
- actions: [
- IconButton(
- tooltip: '稍后再看',
- onPressed: () async {
- var res = await UserHttp
- .toViewLater(
- bvid:
- videoDetailController
- .bvid);
- SmartDialog.showToast(
- res['msg']);
- },
- icon: const Icon(
- Icons.history_outlined),
- ),
- const SizedBox(width: 14)
- ],
+ MaterialStateProperty
+ .resolveWith((states) {
+ return Theme.of(context)
+ .colorScheme
+ .primaryContainer;
+ }),
),
- ),
- Positioned(
- right: 12,
- bottom: 10,
- child: TextButton.icon(
- style: ButtonStyle(
- backgroundColor:
- MaterialStateProperty
- .resolveWith(
- (states) {
- return Theme.of(context)
- .colorScheme
- .primaryContainer;
- }),
- ),
- onPressed: () => handlePlay(),
- icon: const Icon(
- Icons.play_circle_outline,
- size: 20,
- ),
- label: const Text('Play'),
+ onPressed: () => handlePlay(),
+ icon: const Icon(
+ Icons.play_circle_outline,
+ size: 20,
),
+ label: const Text('Play'),
),
- ],
- )),
- ),
- ],
- ),
+ ),
+ ],
+ )),
+ ),
+ ],
);
},
),
diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart
index 0a84a193..8867233c 100644
--- a/lib/pages/video/detail/widgets/header_control.dart
+++ b/lib/pages/video/detail/widgets/header_control.dart
@@ -499,6 +499,28 @@ class _HeaderControlState extends State {
// ),
// fuc: () => _.screenshot(),
// ),
+ SizedBox(
+ width: 34,
+ height: 34,
+ child: Obx(
+ () => IconButton(
+ style: ButtonStyle(
+ padding: MaterialStateProperty.all(EdgeInsets.zero),
+ ),
+ onPressed: () {
+ _.isOpenDanmu.value = !_.isOpenDanmu.value;
+ },
+ icon: Icon(
+ _.isOpenDanmu.value
+ ? Icons.subtitles_outlined
+ : Icons.subtitles_off_outlined,
+ size: 19,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 4),
Obx(
() => SizedBox(
width: 45,
diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart
index 861c7898..84a07070 100644
--- a/lib/plugin/pl_player/controller.dart
+++ b/lib/plugin/pl_player/controller.dart
@@ -11,18 +11,15 @@ import 'package:hive/hive.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:pilipala/http/video.dart';
-import 'package:pilipala/plugin/pl_player/models/data_source.dart';
+import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:universal_platform/universal_platform.dart';
// import 'package:wakelock_plus/wakelock_plus.dart';
-import 'models/data_status.dart';
-import 'models/play_speed.dart';
-import 'models/play_status.dart';
-
Box videoStorage = GStrorage.video;
+Box setting = GStrorage.setting;
class PlPlayerController {
Player? _videoPlayerController;
@@ -84,6 +81,7 @@ class PlPlayerController {
int _cid = 0;
int _heartDuration = 0;
bool _enableHeart = true;
+ bool _isFirstTime = true;
Timer? _timer;
Timer? _timerForSeek;
@@ -105,6 +103,7 @@ class PlPlayerController {
];
PreferredSizeWidget? headerControl;
+ Widget? danmuWidget;
/// 数据加载监听
Stream get onDataStatusChanged => dataStatus.status.stream;
@@ -195,6 +194,9 @@ class PlPlayerController {
///
Rx get videoType => _videoType;
+ /// 弹幕开关
+ Rx isOpenDanmu = true.obs;
+
// 添加一个私有构造函数
PlPlayerController._() {
_videoType = videoType;
@@ -248,6 +250,8 @@ class PlPlayerController {
int cid = 0,
// 历史记录开关
bool enableHeart = true,
+ // 是否首次加载
+ bool isFirstTime = true,
}) async {
try {
_autoPlay = autoplay;
@@ -261,6 +265,7 @@ class PlPlayerController {
_bvid = bvid;
_cid = cid;
_enableHeart = enableHeart;
+ _isFirstTime = isFirstTime;
if (_videoPlayerController != null &&
_videoPlayerController!.state.playing) {
@@ -281,6 +286,12 @@ class PlPlayerController {
if (!_listenersInitialized) {
startListeners();
}
+ bool autoEnterFullcreen =
+ setting.get(SettingBoxKey.enableAutoEnter, defaultValue: false);
+ if (autoEnterFullcreen && _isFirstTime) {
+ await Future.delayed(const Duration(milliseconds: 100));
+ triggerFullScreen();
+ }
} catch (err) {
dataStatus.status.value = DataStatus.error;
print('plPlayer err: $err');
@@ -397,6 +408,8 @@ class PlPlayerController {
}
List subscriptions = [];
+ final List _positionListeners = [];
+ final List _statusListeners = [];
/// 播放事件监听
void startListeners() {
@@ -408,11 +421,21 @@ class PlPlayerController {
} else {
// playerStatus.status.value = PlayerStatus.paused;
}
+
+ /// 触发回调事件
+ for (var element in _statusListeners) {
+ element(event ? PlayerStatus.playing : PlayerStatus.paused);
+ }
makeHeartBeat(_position.value.inSeconds, type: 'status');
}),
videoPlayerController!.stream.completed.listen((event) {
if (event) {
playerStatus.status.value = PlayerStatus.completed;
+
+ /// 触发回调事件
+ for (var element in _statusListeners) {
+ element(PlayerStatus.completed);
+ }
} else {
// playerStatus.status.value = PlayerStatus.playing;
}
@@ -423,6 +446,11 @@ class PlPlayerController {
if (!isSliderMoving.value) {
_sliderPosition.value = event;
}
+
+ /// 触发回调事件
+ for (var element in _positionListeners) {
+ element(event);
+ }
makeHeartBeat(event.inSeconds);
}),
videoPlayerController!.stream.duration.listen((event) {
@@ -714,6 +742,79 @@ class PlPlayerController {
_isFullScreen.value = val;
}
+ // 全屏
+ Future triggerFullScreen({bool status = true}) async {
+ FullScreenMode mode = FullScreenModeCode.fromCode(
+ setting.get(SettingBoxKey.fullScreenMode, defaultValue: 0))!;
+
+ if (!isFullScreen.value && status) {
+ /// 按照视频宽高比决定全屏方向
+ switch (mode) {
+ case FullScreenMode.auto:
+ if (direction.value == 'horizontal') {
+ /// 进入全屏
+ await enterFullScreen();
+ // 横屏
+ await landScape();
+ } else {
+ // 竖屏
+ await verticalScreen();
+ }
+ break;
+ case FullScreenMode.vertical:
+
+ /// 进入全屏
+ await enterFullScreen();
+ // 横屏
+ await verticalScreen();
+ break;
+ case FullScreenMode.horizontal:
+
+ /// 进入全屏
+ await enterFullScreen();
+ // 横屏
+ await landScape();
+ break;
+ }
+
+ toggleFullScreen(true);
+ print(headerControl);
+ print(danmuWidget);
+ var result = await showDialog(
+ context: Get.context!,
+ useSafeArea: false,
+ builder: (context) => Dialog.fullscreen(
+ backgroundColor: Colors.black,
+ child: PLVideoPlayer(
+ controller: this,
+ headerControl: headerControl,
+ danmuWidget: danmuWidget,
+ ),
+ ),
+ );
+ if (result == null) {
+ // 退出全屏
+ exitFullScreen();
+ await verticalScreen();
+ toggleFullScreen(false);
+ }
+ } else if (isFullScreen.value) {
+ Get.back();
+ exitFullScreen();
+ await verticalScreen();
+ toggleFullScreen(false);
+ }
+ }
+
+ void addPositionListener(Function(Duration position) listener) =>
+ _positionListeners.add(listener);
+ void removePositionListener(Function(Duration position) listener) =>
+ _positionListeners.remove(listener);
+ void addStatusLister(Function(PlayerStatus status) listener) =>
+ _statusListeners.add(listener);
+ void removeStatusLister(Function(PlayerStatus status) listener) =>
+ _statusListeners.remove(listener);
+
/// 截屏
Future screenshot() async {
final Uint8List? screenshot =
diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart
index 077b5a1b..67d25223 100644
--- a/lib/plugin/pl_player/view.dart
+++ b/lib/plugin/pl_player/view.dart
@@ -89,6 +89,7 @@ class _PLVideoPlayerState extends State
vsync: this, duration: const Duration(milliseconds: 300));
videoController = widget.controller.videoController!;
widget.controller.headerControl = widget.headerControl;
+ widget.controller.danmuWidget = widget.danmuWidget;
defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior,
defaultValue: BtmProgresBehavior.values.first.code);
@@ -159,67 +160,6 @@ class _PLVideoPlayerState extends State
widget.controller.brightness.value = value;
}
- Future triggerFullScreen() async {
- PlPlayerController _ = widget.controller;
- mode = FullScreenModeCode.fromCode(
- setting.get(SettingBoxKey.fullScreenMode, defaultValue: 0))!;
-
- if (!_.isFullScreen.value) {
- /// 按照视频宽高比决定全屏方向
- switch (mode) {
- case FullScreenMode.auto:
- if (_.direction.value == 'horizontal') {
- /// 进入全屏
- await enterFullScreen();
- // 横屏
- await landScape();
- } else {
- // 竖屏
- await verticalScreen();
- }
- break;
- case FullScreenMode.vertical:
-
- /// 进入全屏
- await enterFullScreen();
- // 横屏
- await verticalScreen();
- break;
- case FullScreenMode.horizontal:
-
- /// 进入全屏
- await enterFullScreen();
- // 横屏
- await landScape();
- break;
- }
-
- _.toggleFullScreen(true);
- var result = await showDialog(
- context: Get.context!,
- useSafeArea: false,
- builder: (context) => Dialog.fullscreen(
- backgroundColor: Colors.black,
- child: PLVideoPlayer(
- controller: _,
- headerControl: _.headerControl,
- ),
- ),
- );
- if (result == null) {
- // 退出全屏
- exitFullScreen();
- await verticalScreen();
- _.toggleFullScreen(false);
- }
- } else {
- Get.back();
- exitFullScreen();
- await verticalScreen();
- _.toggleFullScreen(false);
- }
- }
-
@override
void dispose() {
animationController.dispose();
@@ -472,6 +412,10 @@ class _PLVideoPlayerState extends State
}
}),
+ /// 弹幕面板
+ if (widget.danmuWidget != null)
+ Positioned.fill(top: 4, child: widget.danmuWidget!),
+
/// 手势
Positioned.fill(
left: 16,
@@ -559,13 +503,13 @@ class _PLVideoPlayerState extends State
if (dy > _distance && dy > threshold) {
if (_.isFullScreen.value) {
// 下滑退出全屏
- await triggerFullScreen();
+ await widget.controller.triggerFullScreen(status: false);
}
_distance = 0.0;
} else if (dy < _distance && dy < -threshold) {
if (!_.isFullScreen.value) {
// 上滑进入全屏
- await triggerFullScreen();
+ await widget.controller.triggerFullScreen();
}
_distance = 0.0;
}
@@ -606,7 +550,7 @@ class _PLVideoPlayerState extends State
position: 'bottom',
child: BottomControl(
controller: widget.controller,
- triggerFullScreen: triggerFullScreen),
+ triggerFullScreen: widget.controller.triggerFullScreen),
),
),
],
diff --git a/lib/utils/danmaku.dart b/lib/utils/danmaku.dart
new file mode 100644
index 00000000..a76cc77f
--- /dev/null
+++ b/lib/utils/danmaku.dart
@@ -0,0 +1,24 @@
+import 'package:flutter/material.dart';
+import 'package:ns_danmaku/ns_danmaku.dart';
+
+class DmUtils {
+ static Color decimalToColor(int decimalColor) {
+ int red = (decimalColor >> 16) & 0xFF;
+ int green = (decimalColor >> 8) & 0xFF;
+ int blue = decimalColor & 0xFF;
+
+ return Color.fromARGB(255, red, green, blue);
+ }
+
+ static DanmakuItemType getPosition(int mode) {
+ DanmakuItemType type = DanmakuItemType.scroll;
+ if (mode >= 1 && mode <= 3) {
+ type = DanmakuItemType.scroll;
+ } else if (mode == 4) {
+ type = DanmakuItemType.bottom;
+ } else if (mode == 5) {
+ type = DanmakuItemType.top;
+ }
+ return type;
+ }
+}
diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart
index 21a8060b..91094a39 100644
--- a/lib/utils/storage.dart
+++ b/lib/utils/storage.dart
@@ -97,6 +97,8 @@ class SettingBoxKey {
static const String enableHA = 'enableHA';
static const String enableOnlineTotal = 'enableOnlineTotal';
static const String enableAutoBrightness = 'enableAutoBrightness';
+ static const String enableAutoEnter = 'enableAutoEnter';
+ static const String enableAutoExit = 'enableAutoExit';
/// 隐私
static const String blackMidsList = 'blackMidsList';
diff --git a/pubspec.lock b/pubspec.lock
index 17505881..e97bff54 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -805,6 +805,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.0"
+ ns_danmaku:
+ dependency: "direct main"
+ description:
+ path: "."
+ ref: master
+ resolved-ref: "419a35a776f9784f07999c8f1f75eb26fd9fe90a"
+ url: "https://github.com/xiaoyaocz/flutter_ns_danmaku.git"
+ source: git
+ version: "0.0.5"
octo_image:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index e2610bf5..06d6fbea 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -114,6 +114,12 @@ dependencies:
flutter_displaymode: ^0.6.0
# scheme跳转
appscheme: ^1.0.8
+ # 弹幕
+ ns_danmaku:
+ git:
+ url: https://github.com/xiaoyaocz/flutter_ns_danmaku.git
+ ref: master
+
dev_dependencies:
flutter_test: