248 lines
10 KiB
Dart
248 lines
10 KiB
Dart
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';
|
|
}
|
|
}
|