完善PR审查后出现的问题
This commit is contained in:
@@ -7,6 +7,7 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../widgets/desktop_constrained.dart';
|
||||
import '../../widgets/toast_helper.dart';
|
||||
|
||||
enum _NetworkTestTab { network, service, custom }
|
||||
|
||||
@@ -20,6 +21,13 @@ class NetworkTestPage extends StatefulWidget {
|
||||
class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
static final _panUri = Uri.parse('https://pan.gongyun.org');
|
||||
static final _infoUri = Uri.parse('https://www.gongyun.org/appnetinfo.html');
|
||||
static final _primaryIpLookupUri = Uri.parse('https://ipwho.is/');
|
||||
static final _secondaryIpLookupUri = Uri.parse('https://ipapi.co/json/');
|
||||
static const _probeHeaders = {'Cache-Control': 'no-cache'};
|
||||
static const _probeGetFallbackHeaders = {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Range': 'bytes=0-0',
|
||||
};
|
||||
|
||||
_NetworkTestTab _tab = _NetworkTestTab.network;
|
||||
bool _networkLoading = false;
|
||||
@@ -44,8 +52,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
IconButton(
|
||||
tooltip: '说明',
|
||||
icon: const Icon(Icons.info_outline),
|
||||
onPressed: () =>
|
||||
launchUrl(_infoUri, mode: LaunchMode.externalApplication),
|
||||
onPressed: () => _openExternalUrl(_infoUri),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -71,7 +78,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
ButtonSegment(
|
||||
value: _NetworkTestTab.custom,
|
||||
icon: Icon(Icons.route_outlined),
|
||||
label: Text('自定义'),
|
||||
label: Text('自定义线路'),
|
||||
),
|
||||
],
|
||||
selected: {_tab},
|
||||
@@ -129,7 +136,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('自定义线路'),
|
||||
content: const Text('此功能暂不可用'),
|
||||
content: const Text('自定义线路为会员功能,当前版本暂未开放。'),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
@@ -140,7 +147,20 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openExternalUrl(Uri uri) async {
|
||||
try {
|
||||
final opened = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
if (!opened) {
|
||||
ToastHelper.failure('无法打开链接: $uri');
|
||||
}
|
||||
} catch (e) {
|
||||
ToastHelper.failure('无法打开链接: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runNetworkTest() async {
|
||||
if (_networkLoading) return;
|
||||
|
||||
setState(() {
|
||||
_networkLoading = true;
|
||||
_networkError = null;
|
||||
@@ -171,6 +191,8 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
}
|
||||
|
||||
Future<void> _runServiceTest() async {
|
||||
if (_serviceLoading) return;
|
||||
|
||||
setState(() {
|
||||
_serviceLoading = true;
|
||||
_serviceError = null;
|
||||
@@ -246,7 +268,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
Future<_IpInfo> _fetchIpInfo() async {
|
||||
try {
|
||||
final response = await http
|
||||
.get(Uri.parse('https://ipwho.is/'))
|
||||
.get(_primaryIpLookupUri)
|
||||
.timeout(const Duration(seconds: 8));
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
@@ -270,7 +292,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
|
||||
try {
|
||||
final response = await http
|
||||
.get(Uri.parse('https://ipapi.co/json/'))
|
||||
.get(_secondaryIpLookupUri)
|
||||
.timeout(const Duration(seconds: 8));
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
@@ -307,7 +329,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse('https://status.gongyun.org/api/monitor?pageId=$pageId'),
|
||||
headers: const {'Cache-Control': 'no-cache'},
|
||||
headers: _probeHeaders,
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
@@ -367,13 +389,19 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
}) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
final response = await http
|
||||
.get(uri, headers: const {'Cache-Control': 'no-cache'})
|
||||
var response = await http
|
||||
.head(uri, headers: _probeHeaders)
|
||||
.timeout(const Duration(seconds: 8));
|
||||
if (_shouldRetryProbeWithGet(response.statusCode)) {
|
||||
response = await http
|
||||
.get(uri, headers: _probeGetFallbackHeaders)
|
||||
.timeout(const Duration(seconds: 8));
|
||||
}
|
||||
stopwatch.stop();
|
||||
final isHealthy = healthyStatusCodes.isEmpty
|
||||
? response.statusCode >= 200 && response.statusCode < 500
|
||||
: healthyStatusCodes.contains(response.statusCode);
|
||||
final isHealthy = _isHealthyStatus(
|
||||
response.statusCode,
|
||||
healthyStatusCodes,
|
||||
);
|
||||
return _ProbeResult(
|
||||
reachable: isHealthy,
|
||||
latencyMs: stopwatch.elapsedMilliseconds,
|
||||
@@ -384,6 +412,17 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldRetryProbeWithGet(int statusCode) {
|
||||
return statusCode == 403 || statusCode == 405 || statusCode == 501;
|
||||
}
|
||||
|
||||
bool _isHealthyStatus(int statusCode, Set<int> healthyStatusCodes) {
|
||||
if (healthyStatusCodes.isNotEmpty) {
|
||||
return healthyStatusCodes.contains(statusCode);
|
||||
}
|
||||
return statusCode >= 200 && statusCode < 500;
|
||||
}
|
||||
|
||||
Future<_StabilityResult> _testStability(Uri uri) async {
|
||||
final latencies = <int>[];
|
||||
var successCount = 0;
|
||||
@@ -438,7 +477,7 @@ class _NetworkPanel extends StatelessWidget {
|
||||
if (loading) const LinearProgressIndicator(),
|
||||
if (error != null) _ErrorText(error!),
|
||||
_InfoRow(
|
||||
label: '公云存储官网访问测试',
|
||||
label: '公云存储网盘访问测试',
|
||||
value: data == null ? '检测中' : (data.panReachable ? '正常' : '离线'),
|
||||
valueColor: _statusColor(context, data?.panReachable),
|
||||
),
|
||||
@@ -463,11 +502,14 @@ class _NetworkPanel extends StatelessWidget {
|
||||
title: '说明',
|
||||
children: const [
|
||||
_DescriptionLine('公云存储作为国际化云存储产品,已接入Cloudflare CDN作为全球加速引擎。'),
|
||||
_DescriptionLine(
|
||||
'当前网络 IP 地址、AS 与运营商信息通过第三方服务 ipwho.is 查询;如该服务不可用,将使用 ipapi.co 作为备用查询。',
|
||||
),
|
||||
_DescriptionLine(
|
||||
'由于部分国家/地区有法律法规限制与审查,可能会出现包括但不限于网络波动、网络丢包以及网络连接不稳定现象',
|
||||
),
|
||||
_DescriptionLine(
|
||||
'如诺出现网络连接问题您可将本页面内容向公云存储团队进行报告,团队会根据相关诊断线索进行排查与优化。',
|
||||
'如若出现网络连接问题,您可将本页面内容向公云存储团队报告,团队会根据相关诊断线索进行排查与优化。',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -167,9 +167,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
subtitle: const Text('gongyun_app'),
|
||||
trailing: const Icon(Icons.open_in_new, size: 16),
|
||||
onTap: () {
|
||||
launchUrl(
|
||||
_openExternalUrl(
|
||||
Uri.parse('https://git.saont.net/gongyun/app'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -179,11 +178,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
subtitle: const Text('cloudreve4_flutter'),
|
||||
trailing: const Icon(Icons.open_in_new, size: 16),
|
||||
onTap: () {
|
||||
launchUrl(
|
||||
_openExternalUrl(
|
||||
Uri.parse(
|
||||
'https://github.com/LimoYuan/cloudreve4_flutter',
|
||||
),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -405,13 +403,16 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
return AnimatedBuilder(
|
||||
animation: UpdateService.instance,
|
||||
builder: (context, _) {
|
||||
final hasUpdate = UpdateService.instance.hasUpdate;
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.tag),
|
||||
leading: Icon(
|
||||
hasUpdate ? Icons.new_releases_outlined : Icons.tag,
|
||||
color: hasUpdate ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
title: const Text('版本号'),
|
||||
subtitle: Text(_appVersion.isEmpty ? '加载中...' : _appVersion),
|
||||
trailing: UpdateService.instance.hasUpdate
|
||||
? const _BlinkingUpdateDot()
|
||||
: null,
|
||||
onTap: hasUpdate ? () => _openDownloadsPage() : null,
|
||||
trailing: hasUpdate ? const _UpdateAvailableBadge() : null,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -562,6 +563,25 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openExternalUrl(Uri uri) async {
|
||||
try {
|
||||
final opened = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
if (!opened) {
|
||||
ToastHelper.failure('无法打开链接: $uri');
|
||||
}
|
||||
} catch (e) {
|
||||
ToastHelper.failure('无法打开链接: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openDownloadsPage() async {
|
||||
try {
|
||||
await UpdateService.instance.openDownloadsPage();
|
||||
} catch (e) {
|
||||
ToastHelper.failure('打开下载页面失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openNetworkTest(BuildContext context) async {
|
||||
final agreed = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -569,13 +589,11 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('会话信息授权'),
|
||||
content: _NetworkTestConsentText(
|
||||
onOpenPrivacy: () => launchUrl(
|
||||
onOpenPrivacy: () => _openExternalUrl(
|
||||
Uri.parse('https://www.gongyun.org/policy/privacy.html'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
onOpenTerms: () => launchUrl(
|
||||
onOpenTerms: () => _openExternalUrl(
|
||||
Uri.parse('https://www.gongyun.org/policy/terms.html'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
@@ -641,14 +659,14 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _BlinkingUpdateDot extends StatefulWidget {
|
||||
const _BlinkingUpdateDot();
|
||||
class _UpdateAvailableBadge extends StatefulWidget {
|
||||
const _UpdateAvailableBadge();
|
||||
|
||||
@override
|
||||
State<_BlinkingUpdateDot> createState() => _BlinkingUpdateDotState();
|
||||
State<_UpdateAvailableBadge> createState() => _UpdateAvailableBadgeState();
|
||||
}
|
||||
|
||||
class _BlinkingUpdateDotState extends State<_BlinkingUpdateDot>
|
||||
class _UpdateAvailableBadgeState extends State<_UpdateAvailableBadge>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _opacity;
|
||||
@@ -674,15 +692,31 @@ class _BlinkingUpdateDotState extends State<_BlinkingUpdateDot>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return FadeTransition(
|
||||
opacity: _opacity,
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.error,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'发现新版本',
|
||||
style: TextStyle(
|
||||
color: colorScheme.error,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(Icons.chevron_right, size: 18, color: colorScheme.error),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -734,9 +768,8 @@ class _NetworkTestConsentTextState extends State<_NetworkTestConsentText> {
|
||||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
const TextSpan(text: '您需同意'),
|
||||
TextSpan(
|
||||
@@ -750,7 +783,10 @@ class _NetworkTestConsentTextState extends State<_NetworkTestConsentText> {
|
||||
style: linkStyle,
|
||||
recognizer: _termsRecognizer,
|
||||
),
|
||||
const TextSpan(text: '方可进入网络测试页。'),
|
||||
const TextSpan(
|
||||
text:
|
||||
'方可进入网络测试页。测试会访问公云存储服务,并通过第三方服务 ipwho.is 查询当前网络 IP、AS 与运营商信息;如 ipwho.is 不可用,将使用 ipapi.co 作为备用查询。',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -15,7 +15,16 @@ import '../data/models/app_update_model.dart';
|
||||
import 'storage_service.dart';
|
||||
|
||||
class UpdateService extends ChangeNotifier {
|
||||
UpdateService._();
|
||||
UpdateService._()
|
||||
: _dio = Dio(_dioOptions),
|
||||
_packageInfoProvider = PackageInfo.fromPlatform;
|
||||
|
||||
@visibleForTesting
|
||||
UpdateService.test({
|
||||
Dio? dio,
|
||||
Future<PackageInfo> Function()? packageInfoProvider,
|
||||
}) : _dio = dio ?? Dio(_dioOptions),
|
||||
_packageInfoProvider = packageInfoProvider ?? PackageInfo.fromPlatform;
|
||||
|
||||
static final UpdateService instance = UpdateService._();
|
||||
|
||||
@@ -29,15 +38,16 @@ class UpdateService extends ChangeNotifier {
|
||||
'https://www.gongyun.org/downloads.html',
|
||||
);
|
||||
|
||||
final Dio _dio = Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 12),
|
||||
receiveTimeout: const Duration(seconds: 20),
|
||||
followRedirects: true,
|
||||
responseType: ResponseType.plain,
|
||||
),
|
||||
static final BaseOptions _dioOptions = BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 12),
|
||||
receiveTimeout: const Duration(seconds: 20),
|
||||
followRedirects: true,
|
||||
responseType: ResponseType.plain,
|
||||
);
|
||||
|
||||
final Dio _dio;
|
||||
final Future<PackageInfo> Function() _packageInfoProvider;
|
||||
|
||||
bool _checking = false;
|
||||
AppUpdateInfo? _availableUpdate;
|
||||
|
||||
@@ -49,7 +59,7 @@ class UpdateService extends ChangeNotifier {
|
||||
|
||||
_checking = true;
|
||||
try {
|
||||
final current = await PackageInfo.fromPlatform();
|
||||
final current = await _packageInfoProvider();
|
||||
final latest = await _fetchLatestRelease(currentVersion: current.version);
|
||||
if (latest == null ||
|
||||
_compareVersions(latest.version, current.version) <= 0) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:cloudreve4_flutter/services/update_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('UpdateService', () {
|
||||
test(
|
||||
'checkForUpdate releases checking flag when package info fails',
|
||||
() async {
|
||||
var calls = 0;
|
||||
final service = UpdateService.test(
|
||||
packageInfoProvider: () async {
|
||||
calls += 1;
|
||||
throw Exception('package info unavailable');
|
||||
},
|
||||
);
|
||||
|
||||
expect(await service.checkForUpdate(), isNull);
|
||||
expect(await service.checkForUpdate(), isNull);
|
||||
expect(calls, 2);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user