refa: download video (#1737)

* opt: save pb danmaku

* refa: download video

* opt: replaceAll

* fix: wait delete

* opt: remove completer

* fix: index.json

* tweaks

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
My-Responsitories
2025-11-12 19:12:17 +08:00
committed by GitHub
parent 37b1228552
commit 407b31c5c1
16 changed files with 345 additions and 410 deletions

View File

@@ -1,14 +1,12 @@
import 'dart:convert';
import 'dart:io' show File;
import 'package:PiliPlus/grpc/bilibili/community/service/dm/v1.pb.dart';
import 'package:PiliPlus/grpc/dm.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:fixnum/fixnum.dart' show Int64;
import 'package:PiliPlus/utils/path_utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:path/path.dart' as path;
import 'package:xml/xml.dart';
class PlDanmakuController {
PlDanmakuController(
@@ -36,7 +34,7 @@ class PlDanmakuController {
requestedSeg.clear();
}
int calcSegment(int progress) {
static int calcSegment(int progress) {
return progress ~/ segmentLength;
}
@@ -102,7 +100,7 @@ class PlDanmakuController {
List<DanmakuElem>? getCurrentDanmaku(int progress) {
if (isFileSource) {
initXmlDmIfNeeded();
initFileDmIfNeeded();
} else {
final int segmentIndex = calcSegment(progress);
if (!requestedSeg.contains(segmentIndex)) {
@@ -115,68 +113,24 @@ class PlDanmakuController {
bool closed = false;
late bool _xmlDmLoaded = false;
bool _fileDmLoaded = false;
void initXmlDmIfNeeded() {
if (_xmlDmLoaded) return;
_xmlDmLoaded = true;
_initXmlDm();
void initFileDmIfNeeded() {
if (_fileDmLoaded) return;
_fileDmLoaded = true;
_initFileDm();
}
Future<void> _initXmlDm() async {
Future<void> _initFileDm() async {
try {
final file = File(path.join(plPlayerController.dirPath!, 'danmaku.xml'));
final stream = file.openRead().transform(utf8.decoder);
final buffer = StringBuffer();
await for (final chunk in stream) {
if (closed) {
return;
}
buffer.write(chunk);
}
if (closed) {
return;
}
final xmlString = buffer.toString();
final document = XmlDocument.parse(xmlString);
final danmakus = document.findAllElements('d').toList();
final elems = <DanmakuElem>[];
for (final dm in danmakus) {
if (closed) {
return;
}
try {
final pAttr = dm.getAttribute('p');
if (pAttr != null) {
final parts = pAttr.split(',');
final progress = double.parse(parts[0]); // sec
final mode = int.parse(parts[1]);
final fontsize = int.parse(parts[2]);
final color = int.parse(parts[3]);
// final ctime = int.parse(parts[4]);
// final pool = int.parse(parts[5]);
final midHash = parts[6];
final id = int.parse(parts[7]);
final weight = int.parse(parts[8]);
final content = dm.innerText;
elems.add(
DanmakuElem(
progress: (progress * 1000).toInt(),
mode: mode,
fontsize: fontsize,
color: color,
midHash: midHash,
id: Int64(id),
weight: weight,
content: content,
),
);
}
} catch (_) {
if (kDebugMode) rethrow;
}
}
handleDanmaku(elems);
final file = File(
path.join(plPlayerController.dirPath!, PathUtils.danmakuName),
);
if (!file.existsSync()) return;
final bytes = await file.readAsBytes();
if (bytes.isEmpty) return;
final elem = DmSegMobileReply.fromBuffer(bytes).elems;
handleDanmaku(elem);
} catch (_) {
if (kDebugMode) rethrow;
}

View File

@@ -48,10 +48,10 @@ class _PlDanmakuState extends State<PlDanmaku> {
);
if (playerController.enableShowDanmaku.value) {
if (widget.isFileSource) {
_plDanmakuController.initXmlDmIfNeeded();
_plDanmakuController.initFileDmIfNeeded();
} else {
_plDanmakuController.queryDanmaku(
_plDanmakuController.calcSegment(
PlDanmakuController.calcSegment(
playerController.position.value.inMilliseconds,
),
);

View File

@@ -6,7 +6,6 @@ import 'package:get/get.dart';
class DownloadPageController extends GetxController {
final _downloadService = Get.find<DownloadService>();
late final StreamSubscription _sub;
final pages = RxList<DownloadPageInfo>();
final flag = RxInt(0);
@@ -14,14 +13,12 @@ class DownloadPageController extends GetxController {
void onInit() {
super.onInit();
_loadList();
_sub = _downloadService.downloaFlag.listen((_) {
_loadList();
});
_downloadService.flagNotifier.add(_loadList);
}
@override
void onClose() {
_sub.cancel();
_downloadService.flagNotifier.remove(_loadList);
super.onClose();
}

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
@@ -10,12 +12,15 @@ import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart';
import 'package:PiliPlus/services/download/download_service.dart';
import 'package:PiliPlus/utils/cache_manager.dart';
import 'package:PiliPlus/utils/duration_utils.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/path_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
class DetailItem extends StatelessWidget {
const DetailItem({
@@ -141,11 +146,35 @@ class DetailItem extends StatelessWidget {
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, constraints) => NetworkImgLayer(
src: entry.cover,
width: constraints.maxWidth,
height: constraints.maxHeight,
),
builder: (context, constraints) {
final cover = File(
path.join(entry.entryDirPath, PathUtils.coverName),
);
return cover.existsSync()
? ClipRRect(
borderRadius: StyleString.mdRadius,
child: Image.file(
cover,
width: constraints.maxWidth,
height: constraints.maxHeight,
fit: BoxFit.cover,
cacheHeight: constraints.maxWidth.cacheSize(
context,
),
colorBlendMode: NetworkImgLayer.reduce
? BlendMode.modulate
: null,
color: NetworkImgLayer.reduce
? NetworkImgLayer.reduceLuxColor
: null,
),
)
: NetworkImgLayer(
src: entry.cover,
width: constraints.maxWidth,
height: constraints.maxHeight,
);
},
),
),
if (entry.videoQuality case final videoQuality?)

View File

@@ -89,7 +89,7 @@ List<SettingsModel> get extraSettings => [
if (downloadPath == defPath) return;
downloadPath = defPath;
setState();
Get.find<DownloadService>().readDownloadList();
Get.find<DownloadService>().initDownloadList();
GStorage.setting.delete(SettingBoxKey.downloadPath);
},
dense: true,
@@ -102,7 +102,7 @@ List<SettingsModel> get extraSettings => [
if (path == null || path == downloadPath) return;
downloadPath = path;
setState();
Get.find<DownloadService>().readDownloadList();
Get.find<DownloadService>().initDownloadList();
GStorage.setting.put(SettingBoxKey.downloadPath, path);
},
dense: true,

View File

@@ -130,9 +130,10 @@ class _DownloadPanelState extends State<DownloadPanel> {
style: const TextStyle(height: 1),
strutStyle: const StrutStyle(height: 1, leading: 0),
),
const Icon(
Icon(
size: 18,
Icons.keyboard_arrow_down,
color: theme.colorScheme.onSurfaceVariant,
),
],
),

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
@@ -6,8 +8,11 @@ import 'package:PiliPlus/models/common/video/video_quality.dart';
import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart';
import 'package:PiliPlus/pages/video/introduction/local/controller.dart';
import 'package:PiliPlus/utils/duration_utils.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/path_utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
class LocalIntroPanel extends StatefulWidget {
const LocalIntroPanel({super.key, required this.heroTag});
@@ -49,6 +54,7 @@ class _LocalIntroPanelState extends State<LocalIntroPanel>
BiliDownloadEntryInfo entry,
) {
final outline = theme.colorScheme.outline;
final cover = File(path.join(entry.entryDirPath, PathUtils.coverName));
return Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SizedBox(
@@ -73,11 +79,28 @@ class _LocalIntroPanelState extends State<LocalIntroPanel>
Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: entry.cover,
width: 140.8,
height: 88,
),
cover.existsSync()
? ClipRRect(
borderRadius: StyleString.mdRadius,
child: Image.file(
cover,
width: 140.8,
height: 88,
fit: BoxFit.cover,
cacheHeight: 140.8.cacheSize(context),
colorBlendMode: NetworkImgLayer.reduce
? BlendMode.modulate
: null,
color: NetworkImgLayer.reduce
? NetworkImgLayer.reduceLuxColor
: null,
),
)
: NetworkImgLayer(
src: entry.cover,
width: 140.8,
height: 88,
),
PBadge(
text: DurationUtils.formatDuration(
entry.totalTimeMilli ~/ 1000,

View File

@@ -1308,7 +1308,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
),
if (!videoDetailController.isFileSource)
PopupMenuItem(
onTap: () => videoDetailController.onDownload(context),
onTap: () => videoDetailController.onDownload(this.context),
child: const Text('缓存视频'),
),
if (videoDetailController.cover.value.isNotEmpty)