Files
2026-07-14 11:11:36 +08:00

183 lines
5.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../providers/update_provider.dart';
import '../../shared/app_info.dart';
import '../../shared/utils/format_utils.dart';
class UpdateDialog extends ConsumerStatefulWidget {
const UpdateDialog({super.key});
@override
ConsumerState<UpdateDialog> createState() => _UpdateDialogState();
}
class _UpdateDialogState extends ConsumerState<UpdateDialog> {
bool _installTriggered = false;
@override
Widget build(BuildContext context) {
final state = ref.watch(updateProvider);
final info = state.info;
final appVersion =
ref.watch(appVersionProvider).value ?? kSmPlayerVersionFallback;
if (state.status == UpdateStatus.readyToInstall && !_installTriggered) {
_installTriggered = true;
WidgetsBinding.instance.addPostFrameCallback((_) async {
final navigator = Navigator.of(context);
await ref.read(updateProvider.notifier).install();
if (mounted && navigator.canPop()) {
navigator.pop();
}
});
}
if (info == null) {
return FDialog.adaptive(
title: const Text('检查更新'),
body: _ErrorContent(message: state.error ?? '没有可用的更新信息'),
actions: [
FButton(
onPress: () => ref.read(updateProvider.notifier).manualCheck(),
child: const Text('重试'),
),
FButton(
onPress: () => Navigator.of(context).pop(),
variant: FButtonVariant.outline,
child: const Text('关闭'),
),
],
);
}
final isDownloading = state.status == UpdateStatus.downloading;
final isReady = state.status == UpdateStatus.readyToInstall;
final hasError = state.status == UpdateStatus.error;
final noAsset = info.noAssetForPlatform;
return FDialog.adaptive(
title: Text('发现新版本 ${info.version}'),
body: SizedBox(
width: 460,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('当前版本 $appVersion → 最新版本 ${info.version}'),
const SizedBox(height: 12),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 220),
child: SingleChildScrollView(
child: Text(
info.releaseNotes.trim().isEmpty
? '暂无更新说明'
: info.releaseNotes.trim(),
style: Theme.of(context).textTheme.bodySmall,
),
),
),
if (isDownloading) ...[
const SizedBox(height: 16),
FDeterminateProgress(
value: downloadProgressValue(
received: state.received,
total: state.total,
) ?? 0,
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${formatDownloadBytes(state.received)} / ${formatDownloadBytes(state.total)}',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
formatDownloadSpeed(state.speedBytesPerSec),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
],
if (isReady) ...[
const SizedBox(height: 16),
const Text('安装程序已下载,正在打开。'),
],
if (hasError) ...[
const SizedBox(height: 16),
_ErrorContent(message: state.error ?? '更新失败'),
],
],
),
),
actions: [
if (isDownloading)
FButton(
onPress: () => ref.read(updateProvider.notifier).cancel(),
variant: FButtonVariant.outline,
child: const Text('取消'),
)
else ...[
FButton(
onPress: () => Navigator.of(context).pop(),
variant: FButtonVariant.outline,
child: const Text('稍后'),
),
if (hasError)
FButton(
onPress: () => ref.read(updateProvider.notifier).startDownload(),
child: const Text('重试'),
)
else if (noAsset)
FButton(
onPress: () => _openDownloadPage(info.htmlUrl),
child: const Text('前往下载页'),
)
else
FButton(
onPress: isReady
? null
: () => ref.read(updateProvider.notifier).startDownload(),
child: const Text('下载并安装'),
),
],
],
);
}
Future<void> _openDownloadPage(Uri uri) async {
if (!uri.hasScheme) return;
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
class _ErrorContent extends StatelessWidget {
final String message;
const _ErrorContent({required this.message});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
size: 18,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Flexible(
child: Text(
message,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
);
}
}