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