elysia/lib/page/child/UpdateCheckPage.dart
2025-11-04 09:53:47 +08:00

488 lines
17 KiB
Dart

import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:elysia/plugin/HTTP.dart';
import 'package:elysia/plugin/Remind.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import '../../plugin/ApkInstaller.dart';
import '../../plugin/C.dart';
import 'TargetLog.dart';
class UpdateCheckPage extends StatefulWidget {
@override
_UpdateCheckPageState createState() => _UpdateCheckPageState();
}
class _UpdateCheckPageState extends State<UpdateCheckPage> {
String _currentVersion = '0';
int _currentVersionCode = 0;
bool _isChecking = false;
bool _hasUpdate = false;
String _latestVersion = '0';
String _changelog = '';
double _downloadProgress = 0.0;
bool _isDownloading = false;
int _downloadCode = 0;
String _downloadUrl = "";
@override
void initState() {
super.initState();
_getVersionInfo();
Future.delayed(Duration(milliseconds: 200), () {
_checkForUpdate();
});
}
Future<void> _checkForUpdate() async {
setState(() {
_isChecking = true;
});
try {
dynamic json = await HTTP.create("${C.BASE_URL}/server/latest-version").execute();
if (json["code"] != 200) {
setState(() {
_isChecking = false;
_hasUpdate = false;
});
} else {
_hasUpdate = int.parse(json["data"]["versionCode"].toString()) > _currentVersionCode;
if (_hasUpdate) {
_latestVersion = json["data"]["version"];
_downloadCode = json["data"]["versionCode"];
_downloadUrl = json["data"]["url"];
_changelog = json["data"]["updateInfo"];
}
setState(() {
_isChecking = false;
});
// ✅检查本地文件是否已存在
if (_hasUpdate) {
bool exist = await _isLocalFileExist(_downloadCode);
if (exist) {
setState(() {
_downloadProgress = 1.0;
_isDownloading = false;
});
}
}
}
} catch (_) {
setState(() {
_isChecking = false;
_hasUpdate = false;
});
}
}
Future<void> _startDownload() async {
setState(() {
_downloadProgress = 0.0;
_isDownloading = true;
});
try {
final savePath = await _getLocalFilePath(_downloadCode);
log(savePath);
await Dio().download(
_downloadUrl,
savePath,
onReceiveProgress: (received, total) {
if (total != -1) {
double progress = received / total;
setState(() {
_downloadProgress = progress;
});
}
},
);
setState(() {
_isDownloading = false;
_downloadProgress = 1.0;
});
print('下载完成,文件保存在: $savePath');
_installApk();
} catch (e) {
if (e is DioException && e.type == DioExceptionType.cancel) {
print('下载已取消');
} else {
print('下载失败: $e');
}
setState(() {
_isDownloading = false;
});
}
}
Future<void> _installApk() async {
final path = await _getLocalFilePath(_downloadCode);
if (File(path).existsSync()) {
Remind.show(context, "下载完成", "是否安装?", cancel: "取消", confirm: "现在安装", onTap: (s) async {
if (!s) return;
log(path);
await ApkInstaller.installApk(path);
});
} else {
_startDownload();
}
}
Future<String> _getLocalFilePath(int code) async {
final directory = await getTemporaryDirectory();
String path = '${directory.path}/download/';
final Directory folder = Directory(path);
if (!(await folder.exists())) {
await folder.create(recursive: true);
}
return '${directory.path}/download/$code.apk';
}
Future<bool> _isLocalFileExist(int code) async {
final path = await _getLocalFilePath(code);
return File(path).existsSync();
}
Future<void> _getVersionInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
setState(() {
_currentVersion = packageInfo.version;
_currentVersionCode = int.tryParse(packageInfo.buildNumber) ?? 0;
});
}
static const Duration kFadeDuration = Duration(milliseconds: 300);
static const Duration kSwitchDuration = Duration(milliseconds: 350);
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) {
return;
}
if (_isDownloading) {
Remind.show(context, "退出", "退出后将取消下载,是否退出?", onTap: (s) {
if (s) {
Navigator.pop(context);
}
});
} else {
Navigator.pop(context);
}
},
child: Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
backgroundColor: Colors.grey[100],
elevation: 0,
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () {
if (_isDownloading) {
Remind.show(context, "退出", "退出后将取消下载,是否退出?", onTap: (s) {
if (s) {
Navigator.pop(context);
}
});
} else {
Navigator.pop(context);
}
},
),
title: Text(
"检查更新",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
actions: [
Padding(
padding: EdgeInsets.only(right: 10),
child: GestureDetector(
child: Icon(Icons.info_outline, color: Colors.black),
onTap: () {
Remind.showWidget(context, 'V${_currentVersion}(${_currentVersionCode})',TargetLog(),showCancel: false);
},
),
)
],
),
body: SingleChildScrollView(
child: Column(
children: [
_buildStatusCard(),
SizedBox(height: 16),
AnimatedSwitcher(
duration: kSwitchDuration,
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(begin: Offset(0, 0.03), end: Offset.zero).animate(animation),
child: child,
),
);
},
child: (_hasUpdate && !_isChecking)
? _buildUpdateContent(key: ValueKey('updateContent'))
: SizedBox(key: ValueKey('noUpdateContent'), height: 0),
),
AnimatedSwitcher(
duration: kSwitchDuration,
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
axisAlignment: 1,
sizeFactor: animation,
child: child,
),
);
},
child: _isDownloading ? _buildDownloadProgress(key: ValueKey('downloading')) : SizedBox(key: ValueKey('nodownload'), height: 0),
),
],
),
),
),
);
}
Widget _buildStatusCard() {
return Container(
margin: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
),
child: Column(
children: [
Container(
padding: EdgeInsets.all(24),
child: Column(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.blue[100]!),
),
child: Center(
child: Image.asset('assets/app_icon.png', width: 70, height: 70, fit: BoxFit.cover),
),
),
SizedBox(height: 16),
Text('当前版本', style: TextStyle(fontSize: 14, color: Colors.grey[600])),
SizedBox(height: 4),
Text('V${_currentVersion}(${_currentVersionCode})',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87)),
],
),
),
Divider(height: 1, color: Colors.grey[200]),
Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_isChecking ? '正在检查更新...' : _hasUpdate ? '发现新版本' : '已是最新版本',
key: ValueKey<String>(_isChecking ? 'checking' : (_hasUpdate ? 'hasUpdate' : 'noUpdate')),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _hasUpdate ? Colors.lightBlueAccent : Colors.lightBlueAccent),
),
SizedBox(height: 4),
Text(
_isChecking ? '请稍等片刻' : _hasUpdate ? '立即体验新功能' : '您的应用已更新至最新版本',
key: ValueKey<String>(_isChecking ? 'checkingSub' : (_hasUpdate ? 'hasUpdateSub' : 'noUpdateSub')),
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
),
AnimatedSwitcher(
duration: kSwitchDuration,
transitionBuilder: (child, animation) {
return FadeTransition(opacity: animation, child: ScaleTransition(scale: animation, child: child));
},
child: _isChecking
? SizedBox(width: 32, height: 32, key: ValueKey('checkingIndicator'))
: (!_hasUpdate
? Icon(Icons.check_circle, color: Colors.lightBlueAccent, size: 32, key: ValueKey('checkIcon'))
: Container(
key: ValueKey('newBadge'),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(color: Colors.blue[50], borderRadius: BorderRadius.circular(12)),
child: Text('NEW', style: TextStyle(color: Colors.lightBlueAccent, fontWeight: FontWeight.bold, fontSize: 12)),
)),
),
],
),
),
if (!_isChecking && !_isDownloading)
Container(
padding: EdgeInsets.fromLTRB(16, 0, 16, 16),
child: AnimatedSwitcher(
duration: kSwitchDuration,
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(begin: Offset(0, 0.05), end: Offset.zero).animate(animation), child: child));
},
child: ElevatedButton(
key: ValueKey<String>(
_hasUpdate ? (_downloadProgress == 1.0 ? 'install' : 'update') : 'check',
),
onPressed: _hasUpdate
? (_downloadProgress == 1.0 ? _installApk : _startDownload)
: _checkForUpdate,
style: ElevatedButton.styleFrom(
backgroundColor: _hasUpdate ? Colors.lightBlueAccent : Colors.lightBlueAccent,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 0,
),
child: Text(
_hasUpdate ? (_downloadProgress == 1.0 ? '立即安装' : '立即更新') : '检查更新',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
),
],
),
);
}
Widget _buildUpdateContent({Key? key}) {
return Container(
key: key,
margin: EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(20),
child: Row(
children: [
AnimatedOpacity(duration: kFadeDuration, opacity: 1.0, child: Icon(Icons.new_releases, color: Colors.lightBlueAccent, size: 20)),
SizedBox(width: 8),
Text('更新内容', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87)),
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(color: Colors.blue[50], borderRadius: BorderRadius.circular(8)),
child: Text('V$_latestVersion', style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold)),
),
SizedBox(width: 8),
Text('${_changelog.split('\n').length}项更新', style: TextStyle(color: Colors.grey[600], fontSize: 14)),
],
),
),
SizedBox(height: 16),
AnimatedSize(
duration: kSwitchDuration,
curve: Curves.easeInOut,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text(_changelog, style: TextStyle(fontSize: 15, height: 1.6, color: Colors.grey[700])),
),
),
SizedBox(height: 20),
],
),
);
}
Widget _buildDownloadProgress({Key? key}) {
return Container(
key: key,
margin: EdgeInsets.all(16),
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
),
child: Column(
children: [
Row(
children: [
Icon(Icons.download, color: Colors.blue, size: 20),
SizedBox(width: 8),
Text('下载更新', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
Spacer(),
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: _downloadProgress),
duration: Duration(milliseconds: 300),
builder: (context, value, child) {
return Text('${(value * 100).toStringAsFixed(1)}%', style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold));
},
),
],
),
SizedBox(height: 16),
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: _downloadProgress),
duration: Duration(milliseconds: 300),
builder: (context, value, child) {
return ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: value,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
minHeight: 8,
),
);
},
),
SizedBox(height: 8),
AnimatedSwitcher(
duration: Duration(milliseconds: 250),
child: Text(
_downloadProgress < 1 ? '正在下载更新包...' : '下载完成!',
key: ValueKey<bool>(_downloadProgress < 1),
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
),
],
),
);
}
}