Optimize automatic updates
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import '../../../data/models/user_model.dart';
|
||||
import '../../../data/models/user_setting_model.dart';
|
||||
import '../../../services/update_service.dart';
|
||||
import '../../../services/user_setting_service.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/user_setting_provider.dart';
|
||||
@@ -154,11 +155,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
title: const Text('应用名称'),
|
||||
subtitle: const Text('公云存储'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.tag),
|
||||
title: const Text('版本号'),
|
||||
subtitle: Text(_appVersion.isEmpty ? '加载中...' : _appVersion),
|
||||
),
|
||||
_buildVersionTile(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copyright),
|
||||
title: const Text('License'),
|
||||
@@ -404,6 +401,22 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVersionTile() {
|
||||
return AnimatedBuilder(
|
||||
animation: UpdateService.instance,
|
||||
builder: (context, _) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.tag),
|
||||
title: const Text('版本号'),
|
||||
subtitle: Text(_appVersion.isEmpty ? '加载中...' : _appVersion),
|
||||
trailing: UpdateService.instance.hasUpdate
|
||||
? const _BlinkingUpdateDot()
|
||||
: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogoutButton(BuildContext context, AuthProvider auth) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
@@ -628,6 +641,53 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _BlinkingUpdateDot extends StatefulWidget {
|
||||
const _BlinkingUpdateDot();
|
||||
|
||||
@override
|
||||
State<_BlinkingUpdateDot> createState() => _BlinkingUpdateDotState();
|
||||
}
|
||||
|
||||
class _BlinkingUpdateDotState extends State<_BlinkingUpdateDot>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _opacity;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 900),
|
||||
)..repeat(reverse: true);
|
||||
_opacity = Tween<double>(
|
||||
begin: 0.3,
|
||||
end: 1,
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: _opacity,
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NetworkTestConsentText extends StatefulWidget {
|
||||
final VoidCallback onOpenPrivacy;
|
||||
final VoidCallback onOpenTerms;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../data/models/app_update_model.dart';
|
||||
@@ -31,6 +29,8 @@ class _UpdatePromptState extends State<UpdatePrompt> {
|
||||
|
||||
final update = await UpdateService.instance.checkForUpdate();
|
||||
if (update == null || !mounted) return;
|
||||
final shouldShow = await UpdateService.instance.shouldShowPrompt(update);
|
||||
if (!shouldShow || !mounted) return;
|
||||
|
||||
_dialogVisible = true;
|
||||
try {
|
||||
@@ -41,98 +41,55 @@ class _UpdatePromptState extends State<UpdatePrompt> {
|
||||
}
|
||||
|
||||
Future<void> _showUpdateDialog(AppUpdateInfo update) async {
|
||||
var downloading = false;
|
||||
var progress = 0.0;
|
||||
String? status;
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
final canDownload =
|
||||
update.downloadUrl != null &&
|
||||
(Platform.isAndroid || Platform.isWindows);
|
||||
Future<void> openDownloadsPage() async {
|
||||
try {
|
||||
await UpdateService.instance.openDownloadsPage();
|
||||
if (dialogContext.mounted) Navigator.of(dialogContext).pop();
|
||||
} catch (e) {
|
||||
ToastHelper.failure('打开下载页面失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startUpdate() async {
|
||||
if (downloading || !canDownload) return;
|
||||
Future<void> skipPrompt() async {
|
||||
await UpdateService.instance.skipPromptForFiveDays(update);
|
||||
if (dialogContext.mounted) Navigator.of(dialogContext).pop();
|
||||
}
|
||||
|
||||
setDialogState(() {
|
||||
downloading = true;
|
||||
progress = 0;
|
||||
status = '正在下载更新...';
|
||||
});
|
||||
final releaseNotes = (update.description ?? '').trim();
|
||||
|
||||
try {
|
||||
final file = await UpdateService.instance.downloadUpdate(
|
||||
update,
|
||||
onProgress: (received, total) {
|
||||
if (!mounted || total <= 0) return;
|
||||
setDialogState(() {
|
||||
progress = received / total;
|
||||
});
|
||||
},
|
||||
);
|
||||
if (file == null) return;
|
||||
|
||||
setDialogState(() {
|
||||
progress = 1;
|
||||
status = '下载完成,正在打开安装包...';
|
||||
});
|
||||
await UpdateService.instance.openInstaller(file);
|
||||
if (dialogContext.mounted) Navigator.of(dialogContext).pop();
|
||||
} catch (e) {
|
||||
setDialogState(() {
|
||||
downloading = false;
|
||||
status = '下载或打开安装包失败';
|
||||
});
|
||||
ToastHelper.failure('更新失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('发现新版本 ${update.version}'),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(update.title),
|
||||
if ((update.description ?? '').isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
update.description!,
|
||||
maxLines: 6,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
if (downloading) ...[
|
||||
const SizedBox(height: 16),
|
||||
LinearProgressIndicator(
|
||||
value: progress == 0 ? null : progress,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(status ?? ''),
|
||||
],
|
||||
],
|
||||
),
|
||||
return AlertDialog(
|
||||
title: Text('发现新版本 ${update.version}'),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420, maxHeight: 360),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('新版本号:${update.version}'),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'版本日志',
|
||||
style: Theme.of(dialogContext).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(releaseNotes.isEmpty ? update.title : releaseNotes),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: downloading
|
||||
? null
|
||||
: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: downloading ? null : startUpdate,
|
||||
child: const Text('立即更新'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: skipPrompt, child: const Text('跳过')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(onPressed: openDownloadsPage, child: const Text('更新')),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user