import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../utils/app_update_util.dart'; import '../viewmodels/update_progress_view_model.dart'; class UpdateProgressView extends StatefulWidget { final UpdateInfo update; final bool simulate; final bool simulateError; const UpdateProgressView({ super.key, required this.update, this.simulate = false, this.simulateError = false, }); @override State createState() => _UpdateProgressViewState(); } class _UpdateProgressViewState extends State { late final UpdateProgressViewModel _vm; @override void initState() { super.initState(); _vm = UpdateProgressViewModel( update: widget.update, simulate: widget.simulate, simulateError: widget.simulateError, ); _vm.addListener(_onVmChange); _vm.start(); } void _onVmChange() { if (!mounted) return; if (_vm.phase == OtaPhase.done) { _showRestartDialog(); } else if (_vm.phase == OtaPhase.error) { _showErrorSnackBar(); } } void _showRestartDialog() { showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: const Text('Update ready'), content: const Text( 'The update has been downloaded and installed. ' 'Restart the app now to apply the changes.', ), actions: [ TextButton( onPressed: () { Navigator.pop(context); this.context.go('/bar'); }, child: const Text('Later'), ), FilledButton( onPressed: () { Navigator.pop(context); }, child: const Text('Restart app'), ), ], ), ); } void _showErrorSnackBar() { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(_vm.errorMessage ?? 'Update failed'), backgroundColor: Theme.of(context).colorScheme.error, action: SnackBarAction( label: 'Retry', onPressed: () { _vm.cancel(); _vm.start(); }, ), ), ); } @override void dispose() { _vm.removeListener(_onVmChange); _vm.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.close), onPressed: () => _showCancelDialog(), ), title: const Text('Updating'), centerTitle: true, automaticallyImplyLeading: false, ), body: ListenableBuilder( listenable: _vm, builder: (context, child) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildHeader(), const SizedBox(height: 40), _buildProgress(_vm), const SizedBox(height: 24), _buildStatus(_vm), const Spacer(), _buildVersionInfo(_vm), ], ), ), ), ); }, ), ); } Widget _buildHeader() { return Column( children: [ Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon( Icons.system_update_alt_rounded, size: 56, color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 24), Text( 'KoolTab', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 8), Text( 'Version ${widget.update.version}', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), ], ); } Widget _buildProgress(UpdateProgressViewModel vm) { final scheme = Theme.of(context).colorScheme; return Column( children: [ SizedBox( width: 200, height: 200, child: Stack( alignment: Alignment.center, children: [ SizedBox.expand( child: CircularProgressIndicator( value: vm.phase == OtaPhase.installing ? null : vm.progress, strokeWidth: 10, backgroundColor: scheme.primary.withValues(alpha: 0.12), color: vm.phase == OtaPhase.error ? scheme.error : scheme.primary, ), ), Column( mainAxisSize: MainAxisSize.min, children: [ if (vm.phase == OtaPhase.error) Icon( Icons.error_outline_rounded, size: 48, color: scheme.error, ) else if (vm.phase == OtaPhase.installing) Icon( Icons.settings_rounded, size: 48, color: scheme.primary, ) else Text( '${(vm.progress * 100).toStringAsFixed(0)}%', style: Theme.of(context).textTheme.headlineLarge, ), const SizedBox(height: 4), Text( vm.phase == OtaPhase.installing ? 'Installing' : '', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: scheme.onSurface.withValues(alpha: 0.6), ), ), ], ), ], ), ), const SizedBox(height: 24), if (vm.phase == OtaPhase.downloading) LinearProgressIndicator( value: vm.progress, minHeight: 6, borderRadius: BorderRadius.circular(999), ), ], ); } Widget _buildStatus(UpdateProgressViewModel vm) { final scheme = Theme.of(context).colorScheme; final color = switch (vm.phase) { OtaPhase.error => scheme.error, OtaPhase.done => Colors.green, _ => scheme.onSurface, }; return Text( vm.statusText, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: color), textAlign: TextAlign.center, ); } Widget _buildVersionInfo(UpdateProgressViewModel vm) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.info_outline_rounded, size: 14, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4), ), const SizedBox(width: 6), Text( 'Do not close the app during the update', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4), ), ), ], ); } void _showCancelDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Cancel update?'), content: const Text( 'The update is in progress. If you leave now, ' 'the app may become unstable.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Continue update'), ), FilledButton( style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, ), onPressed: () { Navigator.pop(context); _vm.cancel(); this.context.go('/bar'); }, child: const Text('Cancel update'), ), ], ), ); } }