import 'package:flutter/material.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../theme/theme_extension.dart'; import '../update/client_update_provider.dart'; import '../update/update_info.dart'; final _packageInfoProvider = FutureProvider((_) { return PackageInfo.fromPlatform(); }); class AboutSection extends ConsumerStatefulWidget { const AboutSection({super.key}); @override ConsumerState createState() => _AboutSectionState(); } enum _InstallStage { idle, downloading, error } class _AboutSectionState extends ConsumerState { DateTime? _lastChecked; bool _checking = false; _InstallStage _installStage = _InstallStage.idle; double _installProgress = 0; String? _installError; Future _checkNow() async { setState(() => _checking = true); ref.invalidate(clientUpdateProvider); try { await ref.read(clientUpdateProvider.future); } finally { if (mounted) { setState(() { _checking = false; _lastChecked = DateTime.now(); }); } } } Future _install(UpdateInfo info) async { setState(() { _installStage = _InstallStage.downloading; _installProgress = 0; _installError = null; }); try { final installer = await ref.read(updateInstallerProvider.future); final path = await installer.download( info.apkUrl, onProgress: (p) { if (mounted) setState(() => _installProgress = p); }, ); await installer.install(path); } catch (e) { if (!mounted) return; setState(() { _installStage = _InstallStage.error; _installError = '$e'; }); } } @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; final pkg = ref.watch(_packageInfoProvider); final update = ref.watch(clientUpdateProvider); final installed = pkg.value == null ? '…' : '${pkg.value!.version}+${pkg.value!.buildNumber}'; final UpdateInfo? available = update.value; final hasUpdate = available != null; return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.fromLTRB(16, 4, 16, 8), child: _SectionHeader('About'), ), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 4), child: Row( children: [ Icon(LucideIcons.info, color: fs.parchment, size: 18), const SizedBox(width: 8), Text('Installed version', style: TextStyle(color: fs.parchment, fontSize: 14)), const Spacer(), Text(installed, style: TextStyle( color: fs.ash, fontSize: 13, fontFamily: 'JetBrainsMono')), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 4), child: Row( children: [ Icon(LucideIcons.cloud, color: fs.parchment, size: 18), const SizedBox(width: 8), Text('Latest version', style: TextStyle(color: fs.parchment, fontSize: 14)), const Spacer(), Text( hasUpdate ? available.version : _statusFor(update, installed), style: TextStyle( color: hasUpdate ? fs.accent : fs.ash, fontSize: 13, fontFamily: 'JetBrainsMono'), ), ], ), ), if (_lastChecked != null) Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 0), child: Text( 'Last checked ${_formatTime(_lastChecked!)}', style: TextStyle(color: fs.ash, fontSize: 11), ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: Row( children: [ FilledButton.icon( key: const Key('about_check_for_updates'), onPressed: _checking ? null : _checkNow, style: FilledButton.styleFrom( backgroundColor: fs.accent, foregroundColor: fs.parchment, ), icon: _checking ? SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(fs.parchment), ), ) : const Icon(LucideIcons.refresh_cw, size: 18), label: Text(_checking ? 'Checking…' : 'Check for updates'), ), if (hasUpdate) ...[ const SizedBox(width: 12), FilledButton.icon( key: const Key('about_install_update'), onPressed: _installStage == _InstallStage.downloading ? null : () => _install(available), style: FilledButton.styleFrom( backgroundColor: fs.moss, foregroundColor: fs.parchment, ), icon: _installStage == _InstallStage.downloading ? SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, value: _installProgress > 0 ? _installProgress : null, valueColor: AlwaysStoppedAnimation(fs.parchment), ), ) : const Icon(LucideIcons.download, size: 18), label: Text( _installStage == _InstallStage.downloading ? 'Downloading…' : _installStage == _InstallStage.error ? 'Retry install' : 'Install ${available.version}', ), ), ], ], ), ), if (_installStage == _InstallStage.error && _installError != null) Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Text(_installError!, style: TextStyle(color: fs.error, fontSize: 12), maxLines: 2, overflow: TextOverflow.ellipsis), ), ]); } String _statusFor(AsyncValue update, String installed) { if (update.isLoading) return 'Checking…'; if (update.hasError) return 'Check failed'; return 'Up to date'; } String _formatTime(DateTime t) { final h = t.hour.toString().padLeft(2, '0'); final m = t.minute.toString().padLeft(2, '0'); return '$h:$m'; } } class _SectionHeader extends StatelessWidget { const _SectionHeader(this.label); final String label; @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; return Text( label, style: TextStyle( color: fs.parchment, fontSize: 16, fontWeight: FontWeight.w500, ), ); } }