From e738d58766b3a95b01458e038be24d0cc3b4822c Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 21 Jun 2023 12:40:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=82=E7=9C=8B=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E3=80=81=E8=AE=B0=E5=BD=95=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 4 + lib/http/video.dart | 17 ++++ lib/models/common/business_type.dart | 23 ++++++ lib/pages/history/controller.dart | 1 - lib/pages/history/widgets/item.dart | 104 +++++++++++++++++++++---- lib/pages/search/view.dart | 3 +- lib/pages/video/detail/controller.dart | 24 +++++- lib/pages/video/detail/view.dart | 13 +++- lib/utils/utils.dart | 2 +- 9 files changed, 167 insertions(+), 24 deletions(-) create mode 100644 lib/models/common/business_type.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 02cf07f9..5d6e5813 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -150,4 +150,8 @@ class Api { // 分类搜索 static const String searchByType = '/x/web-interface/search/type'; + + // 记录视频播放进度 + // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md + static const String heartBeat = '/x/click-interface/web/heartbeat'; } diff --git a/lib/http/video.dart b/lib/http/video.dart index af9a3fa1..547bf2ec 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -303,4 +303,21 @@ class VideoHttp { return {'status': true, 'data': []}; } } + + // 视频播放进度 + static Future heartBeat({aid, progress, realtime}) async { + var res = await Request().post(Api.heartBeat, queryParameters: { + 'aid': aid, + // 'bvid': '', + // 'cid': '', + // 'epid': '', + // 'sid': '', + // 'mid': '', + 'played_time': progress, + // 'realtime': realtime, + // 'type': '', + // 'sub_type': '', + 'csrf': await Request.getCsrf(), + }); + } } diff --git a/lib/models/common/business_type.dart b/lib/models/common/business_type.dart new file mode 100644 index 00000000..ed15a268 --- /dev/null +++ b/lib/models/common/business_type.dart @@ -0,0 +1,23 @@ +enum BusinessType { + // 普通视频 + archive, + // 剧集(番剧 / 影视) + pgc, + // 直播 + live, + // 文章 + articleList, + // 文章 + article, + hiddenDurationType, + showBadge +} + +extension BusinessTypeExtension on BusinessType { + String get type => + ['archive', 'pgc', 'live', 'article-list', 'article'][index]; + // 隐藏时长 + List get hiddenDurationType => ['live', 'article-list', 'article']; + // 右上 + List get showBadge => ['pgc', 'article-list', 'article']; +} diff --git a/lib/pages/history/controller.dart b/lib/pages/history/controller.dart index 7f26ba40..d3d340d2 100644 --- a/lib/pages/history/controller.dart +++ b/lib/pages/history/controller.dart @@ -10,7 +10,6 @@ class HistoryController extends GetxController { @override void onInit() { - queryHistoryList(); super.onInit(); } diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart index 26fa674d..9f266e49 100644 --- a/lib/pages/history/widgets/item.dart +++ b/lib/pages/history/widgets/item.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/common/business_type.dart'; import 'package:pilipala/utils/utils.dart'; class HistoryItem extends StatelessWidget { @@ -15,8 +16,22 @@ class HistoryItem extends StatelessWidget { return InkWell( onTap: () async { await Future.delayed(const Duration(milliseconds: 200)); - Get.toNamed('/video?aid=$aid&cid=${videoItem.history.cid}', - arguments: {'heroTag': heroTag, 'pic': videoItem.cover}); + if (videoItem.history.business.contains('article')) { + String cid = videoItem.history.cid != 0 + ? videoItem.history.cid.toString() + : videoItem.history.oid.toString(); + Get.toNamed( + '/webview', + parameters: { + 'url': 'https://www.bilibili.com/read/cv$cid', + 'type': 'note', + 'pageTitle': videoItem.title + }, + ); + } else { + Get.toNamed('/video?aid=$aid&cid=${videoItem.history.cid}', + arguments: {'heroTag': heroTag, 'pic': videoItem.cover}); + } }, child: Column( children: [ @@ -47,27 +62,82 @@ class HistoryItem extends StatelessWidget { child: NetworkImgLayer( // src: videoItem['pic'] + // '@${(maxWidth * 2).toInt()}w', - src: videoItem.cover + '@.webp', + src: (videoItem.cover != '' + ? videoItem.cover + : videoItem.covers.first) + + '@.webp', width: maxWidth, height: maxHeight, ), ), - Positioned( - right: 4, - bottom: 4, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 1, horizontal: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Colors.black54.withOpacity(0.4)), - child: Text( - Utils.timeFormat(videoItem.duration!), - style: const TextStyle( - fontSize: 11, color: Colors.white), + if (!BusinessType + .hiddenDurationType.hiddenDurationType + .contains(videoItem.history.business)) + Positioned( + right: 4, + bottom: 4, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 6), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(4), + color: + Colors.black54.withOpacity(0.4)), + child: Text( + videoItem.progress == -1 + ? '已看完' + : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}', + style: const TextStyle( + fontSize: 11, color: Colors.white), + ), ), ), - ) + // 右上角 + if (BusinessType.showBadge.showBadge + .contains(videoItem.history.business)) + Positioned( + right: 4, + top: 4, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 6), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(4), + color: Theme.of(context) + .colorScheme + .primaryContainer), + child: Text( + videoItem.badge, + style: TextStyle( + fontSize: 11, + color: Theme.of(context) + .colorScheme + .primary), + ), + ), + ), + if (videoItem.history.business == + BusinessType.live.type) + Positioned( + right: 4, + top: 4, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 6), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(4), + color: + Colors.black54.withOpacity(0.4)), + child: Text( + videoItem.badge, + style: const TextStyle( + fontSize: 11, color: Colors.white), + ), + ), + ) ], ); }, diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index ef0d8cdc..4287f72d 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -33,7 +33,8 @@ class _SearchPageState extends State { child: IconButton( onPressed: () => _searchController.submit(), icon: const Icon(CupertinoIcons.search, size: 22)), - ) + ), + const SizedBox(width: 10) ], title: Obx( () => TextField( diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 3e375d27..72f1f74d 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -45,6 +45,8 @@ class VideoDetailController extends GetxController { enabledButtons: const EnabledButtons(pip: true), ); + Timer? timer; + @override void onInit() { super.onInit(); @@ -115,8 +117,6 @@ class VideoDetailController extends GetxController { // log('result: ${result.toString()}'); if (result['status']) { PlayUrlModel data = result['data']; - print(data.dash); - // 指定质量的视频 -> 最高质量的视频 String videoUrl = data.dash!.video!.first.baseUrl!; String audioUrl = data.dash!.audio!.first.baseUrl!; @@ -124,4 +124,24 @@ class VideoDetailController extends GetxController { defaultST: Duration(milliseconds: data.lastPlayTime!)); } } + + void loopHeartBeat() { + timer = Timer.periodic(const Duration(seconds: 5), (timer) { + markHeartBeat(); + }); + } + + void markHeartBeat() async { + Duration progress = meeduPlayerController.position.value; + await VideoHttp.heartBeat(aid: aid, progress: progress.inSeconds); + } + + @override + void onClose() { + markHeartBeat(); + if (timer!.isActive) { + timer!.cancel(); + } + super.onClose(); + } } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 243c543f..52069e3f 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -27,7 +27,7 @@ class _VideoDetailPageState extends State final VideoDetailController videoDetailController = Get.put(VideoDetailController(), tag: Get.arguments['heroTag']); MeeduPlayerController? _meeduPlayerController; - ScrollController _extendNestCtr = ScrollController(); + final ScrollController _extendNestCtr = ScrollController(); late AnimationController animationController; // final _meeduPlayerController = MeeduPlayerController( @@ -46,13 +46,15 @@ class _VideoDetailPageState extends State _meeduPlayerController = videoDetailController.meeduPlayerController; _playerEventSubs = _meeduPlayerController!.onPlayerStatusChanged.listen( (PlayerStatus status) { + videoDetailController.markHeartBeat(); if (status == PlayerStatus.playing) { Wakelock.enable(); - print('开始播放了'); isPlay = false; isShowCover = false; setState(() {}); + videoDetailController.loopHeartBeat(); } else { + videoDetailController.timer!.cancel(); isPlay = true; setState(() {}); Wakelock.disable(); @@ -92,6 +94,7 @@ class _VideoDetailPageState extends State @override void dispose() { videoDetailController.meeduPlayerController.dispose(); + videoDetailController.timer!.cancel(); super.dispose(); } @@ -101,6 +104,9 @@ class _VideoDetailPageState extends State if (!_meeduPlayerController!.pipEnabled) { _meeduPlayerController!.pause(); } + if (videoDetailController.timer!.isActive) { + videoDetailController.timer!.cancel(); + } super.didPushNext(); } @@ -111,6 +117,9 @@ class _VideoDetailPageState extends State await Future.delayed(const Duration(milliseconds: 300)); _meeduPlayerController!.play(); } + if (!videoDetailController.timer!.isActive) { + videoDetailController.loopHeartBeat(); + } super.didPopNext(); } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 962e18ae..61e644d6 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -35,7 +35,7 @@ class Utils { int minute = time ~/ 60; double res = time / 60; if (minute != res) { - return '${minute < 10 ? '0$minute' : minute} :${(time - minute * 60) < 10 ? '0${(time - minute * 60)}' : (time - minute * 60)}'; + return '${minute < 10 ? '0$minute' : minute}:${(time - minute * 60) < 10 ? '0${(time - minute * 60)}' : (time - minute * 60)}'; } else { return '$minute:00'; }