import 'package:abawo_bt_app/model/firmware_file_selection.dart'; import 'package:abawo_bt_app/model/shifter_types.dart'; import 'package:flutter/material.dart'; class FirmwareUpdateFullscreen extends StatelessWidget { const FirmwareUpdateFullscreen({ super.key, required this.progress, required this.selectedFirmware, required this.phaseText, required this.statusText, required this.formattedProgressBytes, required this.expectedOffsetHex, required this.onDismiss, this.doneLabel = 'Done', this.failedLabel = 'Back to device', }); final DfuUpdateProgress progress; final BootloaderDfuPreparedFirmware? selectedFirmware; final String phaseText; final String? statusText; final String formattedProgressBytes; final String expectedOffsetHex; final VoidCallback onDismiss; final String doneLabel; final String failedLabel; bool get _isTerminal => progress.state == DfuUpdateState.completed || progress.state == DfuUpdateState.failed; bool get _isRunning => !_isTerminal && progress.state != DfuUpdateState.idle; String? get _bootloaderStatusText { final status = progress.bootloaderStatus; if (status == null) { return null; } final codeLabel = switch (status.code) { DfuBootloaderStatusCode.ok => 'OK', DfuBootloaderStatusCode.parseError => 'parse error', DfuBootloaderStatusCode.stateError => 'state error', DfuBootloaderStatusCode.boundsError => 'bounds error', DfuBootloaderStatusCode.crcError => 'CRC error', DfuBootloaderStatusCode.flashError => 'flash error', DfuBootloaderStatusCode.unsupportedError => 'unsupported flags', DfuBootloaderStatusCode.vectorError => 'vector table error', DfuBootloaderStatusCode.queueFull => 'queue full', DfuBootloaderStatusCode.bootMetadataError => 'boot metadata error', DfuBootloaderStatusCode.unknown => 'unknown 0x${status.rawCode.toRadixString(16).padLeft(2, '0').toUpperCase()}', }; return '$codeLabel • session ${status.sessionId} • offset 0x${status.expectedOffset.toRadixString(16).padLeft(8, '0').toUpperCase()}'; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final isFailed = progress.state == DfuUpdateState.failed; return PopScope( canPop: false, child: Scaffold( backgroundColor: colorScheme.surface, body: SafeArea( child: Column( children: [ if (_isRunning) Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), color: colorScheme.errorContainer, child: Row( children: [ Icon(Icons.warning_amber_rounded, color: colorScheme.onErrorContainer, size: 20), const SizedBox(width: 10), Expanded( child: Text( 'Do not close the app, lock the phone, or move away from the button.', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onErrorContainer, fontWeight: FontWeight.w600, ), ), ), ], ), ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(24, 32, 24, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( _isTerminal ? (isFailed ? Icons.error_outline_rounded : Icons.check_circle_outline_rounded) : Icons.system_update_alt_rounded, size: 56, color: _isTerminal ? (isFailed ? colorScheme.error : colorScheme.primary) : colorScheme.primary, ), const SizedBox(height: 16), Text( _isTerminal ? (isFailed ? 'Update failed' : 'Update completed') : 'Updating firmware', style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( phaseText, style: theme.textTheme.bodyLarge?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.72), ), ), if (selectedFirmware != null) ...[ const SizedBox(height: 12), Text( '${selectedFirmware!.fileName} • ${_formatBytes(selectedFirmware!.fileBytes.length)}', style: theme.textTheme.bodySmall, ), ], const SizedBox(height: 24), if (_isRunning) ...[ LinearProgressIndicator( value: progress.totalBytes > 0 ? progress.fractionComplete : null, minHeight: 12, borderRadius: BorderRadius.circular(999), ), const SizedBox(height: 12), Text( '${progress.percentComplete}% • $formattedProgressBytes', style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), ], if (progress.state == DfuUpdateState.finishing || progress.state == DfuUpdateState.rebooting || progress.state == DfuUpdateState.verifying) ...[ const SizedBox(height: 20), Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: colorScheme.primaryContainer .withValues(alpha: 0.36), borderRadius: BorderRadius.circular(14), ), child: Row( children: [ SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: colorScheme.primary), ), const SizedBox(width: 10), Expanded( child: Text( 'Bootloader is verifying, resetting, and booting the new app. Keep the screen open.', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.primary, fontWeight: FontWeight.w600, ), ), ), ], ), ), ], if (_bootloaderStatusText != null) ...[ const SizedBox(height: 12), Text( _bootloaderStatusText!, style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.56), ), ), ], if (statusText != null && statusText!.trim().isNotEmpty) ...[ const SizedBox(height: 20), Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: isFailed ? colorScheme.errorContainer : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(14), ), child: Text( statusText!, style: theme.textTheme.bodyMedium?.copyWith( color: isFailed ? colorScheme.onErrorContainer : null, ), ), ), ], if (_isTerminal) ...[ const SizedBox(height: 32), SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: onDismiss, icon: Icon(isFailed ? Icons.arrow_back_rounded : Icons.check_rounded), label: Text(isFailed ? failedLabel : doneLabel), ), ), ], ], ), ), ), ], ), ), ), ); } String _formatBytes(int bytes) { if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB'; } }