chatmcp/lib/widgets/upgrade.dart

364 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/foundation.dart';
import 'package:chatmcp/utils/color.dart';
import 'package:chatmcp/utils/platform.dart';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:chatmcp/generated/app_localizations.dart';
import 'package:chatmcp/widgets/markdown/markit_widget.dart';
class UpgradeNotice extends StatefulWidget {
final Duration checkInterval;
final bool showOnlyOnce;
final String owner;
final String repo;
final List<String> enablePlatforms;
final bool showCheckUpdate;
final bool autoCheck;
const UpgradeNotice({
super.key,
this.checkInterval = const Duration(minutes: 60 * 24),
this.showOnlyOnce = true,
this.owner = 'daodao97',
this.repo = 'chatmcp',
this.enablePlatforms = const ["android", "macos", "windows", "linux"],
this.showCheckUpdate = false,
this.autoCheck = true,
});
@override
State<UpgradeNotice> createState() => _UpgradeNoticeState();
}
class _UpgradeNoticeState extends State<UpgradeNotice> {
bool _hasNewVersion = false;
String _newVersion = '';
String _releaseUrl = '';
String _releaseNotes = '';
Timer? _checkTimer;
bool _isChecking = false;
final String _dismissPrefix = '_update_dismissed_';
@override
void initState() {
super.initState();
// 如果启用了自动检查,则延迟初始检查并设置定时器
if (widget.autoCheck) {
// 延迟初始检查,避免应用启动时的性能影响
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
_checkForUpdates();
}
});
_checkTimer = Timer.periodic(widget.checkInterval, (_) => _checkForUpdates());
}
}
@override
void dispose() {
_checkTimer?.cancel();
super.dispose();
}
Future<void> _checkForUpdates() async {
// 防止多次同时检查
if (_isChecking) return;
setState(() {
_isChecking = true;
});
try {
// 获取本地存储
final prefs = await SharedPreferences.getInstance();
// 获取当前应用版本
final packageInfo = await PackageInfo.fromPlatform();
final currentVersion = packageInfo.version;
debugPrint('Current application version: $currentVersion');
// 检查是否已经通知过用户
final lastNotifiedVersion = prefs.getString('last_shown_version');
if (widget.showOnlyOnce && lastNotifiedVersion != null && prefs.getBool('$_dismissPrefix$lastNotifiedVersion') == true) {
setState(() {
_isChecking = false;
});
return;
}
// 获取GitHub最新release版本
final apiUrl = 'https://api.github.com/repos/${widget.owner}/${widget.repo}/releases/latest';
final response = await http.get(Uri.parse(apiUrl), headers: {'Accept': 'application/vnd.github.v3+json'}).timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final latestVersion = data['tag_name'].toString().replaceAll(RegExp(r'[^0-9.]'), '');
final htmlUrl = data['html_url'] as String;
final body = data['body'] as String?;
debugPrint('Detected latest GitHub version: $latestVersion');
if (_isNewerVersion(currentVersion, latestVersion)) {
if (mounted) {
setState(() {
_hasNewVersion = true;
_newVersion = latestVersion;
_releaseUrl = htmlUrl;
_releaseNotes = body ?? '';
});
// 记录最新通知的版本
await prefs.setString('last_shown_version', latestVersion);
}
} else if (!widget.autoCheck && mounted) {
// 手动检查模式下,如果没有新版本也要显示提示
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("已是最新版本"), duration: const Duration(seconds: 2)));
}
} else {
debugPrint('GitHub API请求失败状态码: ${response.statusCode}');
if (!widget.autoCheck && mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("检查更新失败"), duration: const Duration(seconds: 2)));
}
}
} catch (e) {
debugPrint('检查更新出错: $e');
if (!widget.autoCheck && mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("检查更新失败"), duration: const Duration(seconds: 2)));
}
} finally {
if (mounted) {
setState(() {
_isChecking = false;
});
}
}
}
bool _isNewerVersion(String current, String latest) {
try {
final currentParts = current.split('.').map(int.parse).toList();
final latestParts = latest.split('.').map(int.parse).toList();
// 确保两个列表的长度相同
while (currentParts.length < 3) {
currentParts.add(0);
}
while (latestParts.length < 3) {
latestParts.add(0);
}
// 逐位比较版本号
for (var i = 0; i < 3; i++) {
if (latestParts[i] > currentParts[i]) {
return true;
} else if (latestParts[i] < currentParts[i]) {
return false;
}
}
return false;
} catch (e) {
debugPrint('版本比较错误: $e');
return false;
}
}
Future<void> _openReleaseUrl() async {
if (_releaseUrl.isNotEmpty) {
final url = Uri.parse(_releaseUrl);
try {
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.openUrlFailed)));
}
}
} catch (e) {
debugPrint('打开URL错误: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.openUrlFailed)));
}
}
}
}
void _dismissUpdate() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('$_dismissPrefix$_newVersion', true);
if (mounted) {
setState(() {
_hasNewVersion = false;
});
}
}
@override
Widget build(BuildContext context) {
if (kIsWeb && !widget.enablePlatforms.contains("web")) {
return const SizedBox.shrink();
}
if (kIsIOS && !widget.enablePlatforms.contains("ios")) {
return const SizedBox.shrink();
}
if (kIsAndroid && !widget.enablePlatforms.contains("android")) {
return const SizedBox.shrink();
}
if (kIsMacOS && !widget.enablePlatforms.contains("macos")) {
return const SizedBox.shrink();
}
if (kIsWindows && !widget.enablePlatforms.contains("windows")) {
return const SizedBox.shrink();
}
if (kIsLinux && !widget.enablePlatforms.contains("linux")) {
return const SizedBox.shrink();
}
final l10n = AppLocalizations.of(context)!;
final MediaQueryData mediaQuery = MediaQuery.of(context);
final bool isVeryNarrow = mediaQuery.size.width < 400;
final bool isNarrow = mediaQuery.size.width < 600 && !isVeryNarrow;
// 如果有新版本,显示更新通知
if (_hasNewVersion) {
return GestureDetector(
onTap: () => _showUpdateDialog(context),
child: Container(
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isVeryNarrow)
const Icon(Icons.new_releases, size: 14, color: Colors.red)
else ...[
const Icon(Icons.arrow_downward, size: 16, color: Colors.red),
const SizedBox(width: 4),
if (isNarrow)
const Icon(Icons.new_releases, size: 12, color: Colors.red)
else
Flexible(
child: Text(
l10n.newVersionFound(_newVersion),
style: const TextStyle(color: Colors.red, fontSize: 12, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
],
),
),
);
}
// 如果启用手动检查更新,且没有新版本,显示检查更新按钮
if (widget.showCheckUpdate) {
return GestureDetector(
onTap: _isChecking ? null : () => _checkForUpdates(),
child: Container(
decoration: BoxDecoration(
color: AppColors.getInkIconHoverColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_isChecking
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.update, size: 14, color: Colors.grey),
if (!isVeryNarrow) ...[
const SizedBox(width: 4),
Text(
_isChecking ? l10n.checkingForUpdates : l10n.checkUpdate,
style: TextStyle(color: AppColors.getThemeTextColor(context), fontSize: 12),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
],
),
),
);
}
return const SizedBox.shrink();
}
Future<void> _showUpdateDialog(BuildContext context) async {
final l10n = AppLocalizations.of(context)!;
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Row(
children: [
const Icon(Icons.new_releases, color: Colors.red),
const SizedBox(width: 8),
Text(l10n.newVersionFound(_newVersion)),
],
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.newVersionAvailable),
if (_releaseNotes.isNotEmpty) ...[
const SizedBox(height: 16),
Text(l10n.releaseNotes, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Theme.of(context).dividerColor),
),
child: Markit(data: _releaseNotes, textStyle: const TextStyle(fontSize: 12)),
),
],
],
),
),
actions: <Widget>[
TextButton(
onPressed: () {
_dismissUpdate();
Navigator.of(context).pop();
},
child: Text(l10n.ignoreThisVersion),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(l10n.updateLater),
),
FilledButton(
onPressed: () {
_openReleaseUrl();
Navigator.of(context).pop();
},
child: Text(l10n.updateNow),
),
],
);
},
);
}
}