feat: video download

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-11-06 12:12:32 +08:00
parent 976622df89
commit ffd4f9ee73
92 changed files with 4853 additions and 946 deletions

View File

@@ -25,15 +25,18 @@ import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart';
import 'package:PiliPlus/pages/setting/widgets/slide_dialog.dart';
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart';
import 'package:PiliPlus/services/download/download_service.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/cache_manage.dart';
import 'package:PiliPlus/utils/cache_manager.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/path_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/update.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -43,7 +46,7 @@ import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
List<SettingsModel> get extraSettings => [
if (Utils.isDesktop)
if (Utils.isDesktop) ...[
SettingsModel(
settingsType: SettingsType.sw1tch,
title: '退出时最小化',
@@ -56,6 +59,63 @@ List<SettingsModel> get extraSettings => [
} catch (_) {}
},
),
SettingsModel(
settingsType: SettingsType.normal,
title: '缓存路径',
getSubtitle: () => downloadPath,
leading: const Icon(Icons.storage),
onTap: (setState) {
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
onTap: () {
Get.back();
Utils.copyText(downloadPath);
},
dense: true,
title: const Text('复制', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () {
Get.back();
final defPath = defDownloadPath;
if (downloadPath == defPath) return;
downloadPath = defPath;
setState();
Get.find<DownloadService>().readDownloadList();
GStorage.setting.delete(SettingBoxKey.downloadPath);
},
dense: true,
title: const Text('重置', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () async {
Get.back();
final path = await FilePicker.platform.getDirectoryPath();
if (path == null || path == downloadPath) return;
downloadPath = path;
setState();
Get.find<DownloadService>().readDownloadList();
GStorage.setting.put(SettingBoxKey.downloadPath, path);
},
dense: true,
title: const Text('设置新路径', style: TextStyle(fontSize: 14)),
),
],
),
);
},
);
},
),
],
SettingsModel(
settingsType: SettingsType.sw1tch,
title: '空降助手',
@@ -1104,7 +1164,7 @@ List<SettingsModel> get extraSettings => [
title: '最大缓存大小',
getSubtitle: () {
final num = Pref.maxCacheSize;
return '当前最大缓存大小: 「${num == 0 ? '无限' : CacheManage.formatSize(Pref.maxCacheSize)}';
return '当前最大缓存大小: 「${num == 0 ? '无限' : CacheManager.formatSize(Pref.maxCacheSize)}';
},
onTap: (setState) {
showDialog(

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/ua_type.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/common/video/cdn_type.dart';
import 'package:PiliPlus/models/common/video/video_type.dart';
@@ -80,18 +81,19 @@ class CdnSelectDialog extends StatefulWidget {
class _CdnSelectDialogState extends State<CdnSelectDialog> {
late final List<ValueNotifier<String?>> _cdnResList;
late final CancelToken _cancelToken;
late final List<CancelToken?> _tokens;
late final bool _cdnSpeedTest;
@override
void initState() {
_cdnSpeedTest = Pref.cdnSpeedTest;
if (_cdnSpeedTest) {
final length = CDNService.values.length;
_cdnResList = List.generate(
CDNService.values.length,
length,
(_) => ValueNotifier<String?>(null),
);
_cancelToken = CancelToken();
_tokens = List.generate(length, (_) => CancelToken());
_startSpeedTest();
}
super.initState();
@@ -100,10 +102,13 @@ class _CdnSelectDialogState extends State<CdnSelectDialog> {
@override
void dispose() {
if (_cdnSpeedTest) {
_cancelToken.cancel();
for (final e in _tokens) {
e?.cancel();
}
for (final notifier in _cdnResList) {
notifier.dispose();
}
_dio.close(force: true);
}
super.dispose();
}
@@ -145,26 +150,38 @@ class _CdnSelectDialogState extends State<CdnSelectDialog> {
}
}
late final _dio = Dio()
..options.headers = {
'user-agent': UaType.pc.ua,
'referer': HttpString.baseUrl,
};
Future<void> _measureDownloadSpeed(String url, int index) async {
const maxSize = 8 * 1024 * 1024;
int downloaded = 0;
final dio = Dio()..options.headers['referer'] = HttpString.baseUrl;
final cancelToken = _tokens[index];
final start = DateTime.now().microsecondsSinceEpoch;
await dio.get(
void onClose() {
cancelToken?.cancel();
_tokens[index] = null;
}
await _dio.get(
url,
cancelToken: _cancelToken,
cancelToken: cancelToken,
onReceiveProgress: (count, total) {
if (!mounted) {
dio.close(force: true);
return;
}
final duration = DateTime.now().microsecondsSinceEpoch - start;
downloaded += count;
if (duration > 15000000) {
dio.close(force: true);
onClose();
if (downloaded > 0) {
_updateSpeedResult(index, downloaded, duration);
downloaded = 0;
@@ -172,7 +189,7 @@ class _CdnSelectDialogState extends State<CdnSelectDialog> {
throw TimeoutException('测速超时');
}
} else if (downloaded >= maxSize) {
dio.close(force: true);
onClose();
_updateSpeedResult(index, downloaded, duration);
downloaded = 0;
}
@@ -186,6 +203,9 @@ class _CdnSelectDialogState extends State<CdnSelectDialog> {
}
void _handleSpeedTestError(dynamic error, int index) {
_tokens
..[index]?.cancel()
..[index] = null;
final item = _cdnResList[index];
if (item.value != null) return;