mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
opt: select dialog & feat: select subtitle if muted (#564)
* opt: select dialog * opt: subtitle * feat: select subtitle if muted
This commit is contained in:
committed by
GitHub
parent
2ddfea5cf3
commit
82f9f48a8e
@@ -1,137 +1,200 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
import 'package:PiliPlus/models/video/play/CDN.dart';
|
||||
import 'package:PiliPlus/models/video/play/url.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/video_utils.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get_utils/get_utils.dart';
|
||||
|
||||
class SelectDialog<T> extends StatefulWidget {
|
||||
final T value;
|
||||
class SelectDialog<T> extends StatelessWidget {
|
||||
final T? value;
|
||||
final String title;
|
||||
final List<dynamic> values;
|
||||
final List<(T, String)> values;
|
||||
final Widget Function(BuildContext, int)? subtitleBuilder;
|
||||
|
||||
const SelectDialog({
|
||||
super.key,
|
||||
required this.value,
|
||||
this.value,
|
||||
required this.values,
|
||||
required this.title,
|
||||
this.subtitleBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SelectDialog<T>> createState() => _SelectDialogState<T>();
|
||||
}
|
||||
|
||||
class _SelectDialogState<T> extends State<SelectDialog<T>> {
|
||||
late T _tempValue;
|
||||
late List _cdnResList;
|
||||
late final cdnSpeedTest = GStorage.cdnSpeedTest;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tempValue = widget.value;
|
||||
if (widget.title == 'CDN 设置' && cdnSpeedTest) {
|
||||
_cdnResList = List.generate(widget.values.length, (_) => null);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
dynamic result = await VideoHttp.videoUrl(
|
||||
cid: 196018899,
|
||||
bvid: 'BV1fK4y1t7hj',
|
||||
);
|
||||
if (result['status']) {
|
||||
VideoItem videoItem = result['data'].dash.video.first;
|
||||
|
||||
for (CDNService item in CDNService.values) {
|
||||
if (mounted.not) {
|
||||
break;
|
||||
}
|
||||
String videoUrl = VideoUtils.getCdnUrl(videoItem, item.code);
|
||||
Dio dio = Dio()..options.headers['referer'] = HttpString.baseUrl;
|
||||
int maxSize = 8 * 1024 * 1024;
|
||||
int downloaded = 0;
|
||||
int start = DateTime.now().millisecondsSinceEpoch;
|
||||
try {
|
||||
await dio.get(
|
||||
videoUrl,
|
||||
onReceiveProgress: (int count, int total) {
|
||||
downloaded += count;
|
||||
int now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (now - start > 15 * 1000) {
|
||||
dio.close(force: true);
|
||||
}
|
||||
if (downloaded >= maxSize) {
|
||||
dio.close(force: true);
|
||||
_cdnResList[item.index] =
|
||||
(maxSize / (now - start) / 1000).toPrecision(2);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
if (_cdnResList[item.index] == null) {
|
||||
_cdnResList[item.index] = '测速失败';
|
||||
debugPrint('$e');
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('failed to check: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
title: Text(widget.title),
|
||||
title: Text(title),
|
||||
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
|
||||
content: StatefulBuilder(builder: (context, StateSetter setState) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(
|
||||
widget.values.length,
|
||||
(index) => RadioListTile(
|
||||
dense: true,
|
||||
value: widget.values[index]['value'],
|
||||
title: Text(
|
||||
widget.values[index]['title'],
|
||||
style: Theme.of(context).textTheme.titleMedium!,
|
||||
),
|
||||
subtitle: widget.title == 'CDN 设置' && cdnSpeedTest
|
||||
? Text(
|
||||
_cdnResList[index] is double
|
||||
? '${_cdnResList[index]} MB/s'
|
||||
: _cdnResList[index] is String
|
||||
? _cdnResList[index]
|
||||
: '---',
|
||||
style: TextStyle(fontSize: 13),
|
||||
)
|
||||
: null,
|
||||
groupValue: _tempValue,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_tempValue = value as T;
|
||||
});
|
||||
Navigator.pop(context, _tempValue);
|
||||
},
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(
|
||||
values.length,
|
||||
(index) => RadioListTile<T>(
|
||||
dense: true,
|
||||
value: values[index].$1,
|
||||
title: Text(
|
||||
values[index].$2,
|
||||
style: Theme.of(context).textTheme.titleMedium!,
|
||||
),
|
||||
subtitle: subtitleBuilder?.call(context, index),
|
||||
groupValue: value,
|
||||
onChanged: Navigator.of(context).pop,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CdnSelectDialog extends StatefulWidget {
|
||||
final VideoItem? sample;
|
||||
|
||||
const CdnSelectDialog({
|
||||
super.key,
|
||||
this.sample,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CdnSelectDialog> createState() => _CdnSelectDialogState();
|
||||
}
|
||||
|
||||
class _CdnSelectDialogState extends State<CdnSelectDialog> {
|
||||
late final List<ValueNotifier<String?>> _cdnResList;
|
||||
late final CancelToken _cancelToken;
|
||||
bool _cdnSpeedTest = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_cdnSpeedTest = GStorage.cdnSpeedTest;
|
||||
if (_cdnSpeedTest) {
|
||||
_startSpeedTest();
|
||||
_cdnResList = List.generate(
|
||||
CDNService.values.length, (_) => ValueNotifier<String?>(null));
|
||||
_cancelToken = CancelToken();
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_cdnSpeedTest) {
|
||||
_cancelToken.cancel();
|
||||
for (final notifier in _cdnResList) {
|
||||
notifier.dispose();
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<VideoItem> _getSampleUrl() async {
|
||||
final result =
|
||||
await VideoHttp.videoUrl(cid: 196018899, bvid: 'BV1fK4y1t7hj');
|
||||
if (!result['status']) throw Exception('无法获取视频流');
|
||||
return result['data'].dash.video.first;
|
||||
}
|
||||
|
||||
Future<void> _startSpeedTest() async {
|
||||
try {
|
||||
final videoItem = widget.sample ?? await _getSampleUrl();
|
||||
await _testAllCdnServices(videoItem);
|
||||
} catch (e) {
|
||||
debugPrint('CDN speed test failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _testAllCdnServices(VideoItem videoItem) async {
|
||||
for (final item in CDNService.values) {
|
||||
if (!mounted) break;
|
||||
await _testSingleCdn(item, videoItem);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _testSingleCdn(CDNService item, VideoItem videoItem) async {
|
||||
try {
|
||||
final cdnUrl = VideoUtils.getCdnUrl(videoItem, item.code);
|
||||
await _measureDownloadSpeed(cdnUrl, item.index);
|
||||
} catch (e) {
|
||||
_handleSpeedTestError(e, item.index);
|
||||
}
|
||||
}
|
||||
|
||||
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 start = DateTime.now().microsecondsSinceEpoch;
|
||||
|
||||
await dio.get(
|
||||
url,
|
||||
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);
|
||||
if (downloaded > 0) {
|
||||
_updateSpeedResult(index, downloaded, duration);
|
||||
downloaded = 0;
|
||||
} else {
|
||||
throw TimeoutException('测速超时');
|
||||
}
|
||||
} else if (downloaded >= maxSize) {
|
||||
dio.close(force: true);
|
||||
_updateSpeedResult(index, downloaded, duration);
|
||||
downloaded = 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateSpeedResult(int index, int downloaded, int duration) {
|
||||
final speed = (downloaded / duration).toStringAsPrecision(3);
|
||||
_cdnResList[index].value = '${speed}MB/s';
|
||||
}
|
||||
|
||||
void _handleSpeedTestError(dynamic error, int index) {
|
||||
if (_cdnResList[index].value != null) return;
|
||||
|
||||
debugPrint('CDN speed test error: $error');
|
||||
if (!mounted) return;
|
||||
var message = error.toString();
|
||||
if (message.length > 30) {
|
||||
message = '${message.substring(0, 30)}...';
|
||||
} else if (message.isEmpty) {
|
||||
message = '测速失败';
|
||||
}
|
||||
_cdnResList[index].value = message;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectDialog<String>(
|
||||
title: 'CDN 设置',
|
||||
values: CDNService.values.map((i) => (i.code, i.description)).toList(),
|
||||
value: GStorage.defaultCDNService,
|
||||
subtitleBuilder: _cdnSpeedTest
|
||||
? (context, index) => ValueListenableBuilder(
|
||||
valueListenable: _cdnResList[index],
|
||||
builder: (context, value, _) {
|
||||
return Text(
|
||||
_cdnResList[index].value ?? '---',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user