feat: 观看历史分类、记录视频播放进度

This commit is contained in:
guozhigq
2023-06-21 12:40:21 +08:00
parent fe93eb690c
commit e738d58766
9 changed files with 167 additions and 24 deletions

View File

@ -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';
}

View File

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

View File

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

View File

@ -10,7 +10,6 @@ class HistoryController extends GetxController {
@override
void onInit() {
queryHistoryList();
super.onInit();
}

View File

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

View File

@ -33,7 +33,8 @@ class _SearchPageState extends State<SearchPage> {
child: IconButton(
onPressed: () => _searchController.submit(),
icon: const Icon(CupertinoIcons.search, size: 22)),
)
),
const SizedBox(width: 10)
],
title: Obx(
() => TextField(

View File

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

View File

@ -27,7 +27,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
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<VideoDetailPage>
_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<VideoDetailPage>
@override
void dispose() {
videoDetailController.meeduPlayerController.dispose();
videoDetailController.timer!.cancel();
super.dispose();
}
@ -101,6 +104,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
if (!_meeduPlayerController!.pipEnabled) {
_meeduPlayerController!.pause();
}
if (videoDetailController.timer!.isActive) {
videoDetailController.timer!.cancel();
}
super.didPushNext();
}
@ -111,6 +117,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
await Future.delayed(const Duration(milliseconds: 300));
_meeduPlayerController!.play();
}
if (!videoDetailController.timer!.isActive) {
videoDetailController.loopHeartBeat();
}
super.didPopNext();
}

View File

@ -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';
}