feat: fix debug updater
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
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<UpdateProgressView> createState() => _UpdateProgressViewState();
|
||||
}
|
||||
|
||||
class _UpdateProgressViewState extends State<UpdateProgressView> {
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user