import 'package:flutter/material.dart'; import '../utils/app_update_util.dart'; import '../utils/navigation.dart'; import '../viewmodels/update_progress_view_model.dart'; import '../l10n/app_localizations.dart'; import '../l10n/app_localizations_helpers.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() { final l10n = AppLocalizations.of(context); showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: Text(l10n.updateReady), content: Text(l10n.updateReadyDescription), actions: [ TextButton( onPressed: () { Navigator.pop(context); this.context.popOrGo('/bar'); }, child: Text(l10n.later), ), FilledButton( onPressed: () { Navigator.pop(context); }, child: Text(l10n.restartApp), ), ], ), ); } void _showErrorSnackBar() { final l10n = AppLocalizations.of(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(l10n.updateError(_vm.errorMessage)), backgroundColor: Theme.of(context).colorScheme.error, action: SnackBarAction( label: l10n.retry, onPressed: () { _vm.cancel(); _vm.start(); }, ), ), ); } @override void dispose() { _vm.removeListener(_onVmChange); _vm.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (!didPop) _showCancelDialog(); }, child: Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.close), onPressed: () => _showCancelDialog(), ), title: Text(l10n.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() { final l10n = AppLocalizations.of(context); 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( l10n.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; final l10n = AppLocalizations.of(context); 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 ? l10n.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 l10n = AppLocalizations.of(context); final color = switch (vm.phase) { OtaPhase.error => scheme.error, OtaPhase.done => Colors.green, _ => scheme.onSurface, }; return Text( l10n.updateStatus(vm.statusText), style: Theme.of(context).textTheme.titleMedium?.copyWith(color: color), textAlign: TextAlign.center, ); } Widget _buildVersionInfo(UpdateProgressViewModel vm) { final l10n = AppLocalizations.of(context); 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( l10n.doNotCloseDuringUpdate, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.4), ), ), ], ); } void _showCancelDialog() { final l10n = AppLocalizations.of(context); showDialog( context: context, builder: (context) => AlertDialog( title: Text(l10n.cancelUpdate), content: Text(l10n.cancelUpdateDescription), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.continueUpdate), ), FilledButton( style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, ), onPressed: () { Navigator.pop(context); _vm.cancel(); this.context.popOrGo('/bar'); }, child: Text(l10n.cancelUpdateAction), ), ], ), ); } }