feat(studio): real install progress in the store dialog and the wizard
Both surfaces showed an indeterminate spinner for the whole install, because the hub only offered a unary call. They now follow HubAdmin/InstallModuleStream: - The store's install dialog shows the phase in plain language, the overall percentage, megabytes while the size is known, and a seconds counter. The counter is the second, independent signal: it keeps running even if the hub goes quiet, which is what tells 'slow' apart from 'stuck'. - The setup wizard shows the same bar per module instead of a spinner inside the button. - New strings in both locales for the phases, the byte line and the elapsed counter. progress_bar_visual_test writes the four waiting states to build/progress/ in either theme, so the result can be judged without launching the desktop app. Writing it turned up a real trap worth recording: awaiting toImage() directly in a widget test leaves the binding waiting forever - the file passed in six seconds, then sat there until the ten-minute timeout failed the whole suite. tester.runAsync() is the fix. Full suite: 186 passed. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
2bc556e0f5
commit
1549334767
9 changed files with 490 additions and 74 deletions
|
|
@ -2847,15 +2847,83 @@ class _InstallProgressDialog extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _InstallProgressDialogState extends State<_InstallProgressDialog> {
|
||||
late final Future<({String name, String version})> _future;
|
||||
StreamSubscription<InstallStep>? _sub;
|
||||
Timer? _clock;
|
||||
final Stopwatch _elapsed = Stopwatch();
|
||||
|
||||
/// Latest observation from the hub. Null until the first one lands.
|
||||
InstallStep? _step;
|
||||
({String name, String version})? _result;
|
||||
Object? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_elapsed.start();
|
||||
// A second, independent signal that something is happening: the
|
||||
// elapsed clock keeps counting even if the hub goes quiet, which
|
||||
// is what tells an operator "slow" apart from "stuck".
|
||||
_clock = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
// Capability-name install: hub resolves the wasm_url from the
|
||||
// bundled store-index. No source prompt needed for entries
|
||||
// whose seed.yaml already has wasm_url.
|
||||
_future = HubService.instance.installModule(source: widget.item.name);
|
||||
_sub = HubService.instance
|
||||
.installModuleStreaming(
|
||||
source: widget.item.name,
|
||||
onFinished: (r) {
|
||||
if (mounted) setState(() => _result = r);
|
||||
},
|
||||
)
|
||||
.listen(
|
||||
(step) {
|
||||
if (mounted) setState(() => _step = step);
|
||||
},
|
||||
onError: (Object e) {
|
||||
if (mounted) setState(() => _error = e);
|
||||
_stopClock();
|
||||
},
|
||||
onDone: _stopClock,
|
||||
);
|
||||
}
|
||||
|
||||
void _stopClock() {
|
||||
_elapsed.stop();
|
||||
_clock?.cancel();
|
||||
_clock = null;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub?.cancel();
|
||||
_clock?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Plain-language label for the phase the hub last reported.
|
||||
String _stageLabel(AppLocalizations l) {
|
||||
if (_result != null) return l.installPhaseDone;
|
||||
switch (_step?.phase) {
|
||||
case 'download':
|
||||
return l.installPhaseDownload;
|
||||
case 'install':
|
||||
return l.installPhaseInstall;
|
||||
case 'resolve':
|
||||
return l.installPhaseResolve;
|
||||
default:
|
||||
return l.installPhaseResolve;
|
||||
}
|
||||
}
|
||||
|
||||
/// "3,4 of 12,0 MB" while the size is known, otherwise null so the
|
||||
/// bar falls back to showing the percentage.
|
||||
String? _detail(AppLocalizations l) {
|
||||
final step = _step;
|
||||
if (step == null || !step.hasSize || step.phase != 'download') return null;
|
||||
String mb(int bytes) => (bytes / 1048576).toStringAsFixed(1);
|
||||
return l.installBytes(mb(step.bytesReceived), mb(step.bytesTotal));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -2869,64 +2937,7 @@ class _InstallProgressDialogState extends State<_InstallProgressDialog> {
|
|||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480, maxHeight: 360),
|
||||
child: FutureBuilder<({String name, String version})>(
|
||||
future: _future,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState == ConnectionState.waiting) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: ChainSpace.md),
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: ChainSpace.md),
|
||||
Text(
|
||||
l.storeInstallProgressBody,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (snap.hasError) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: theme.colorScheme.error,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: ChainSpace.md),
|
||||
ChainErrorBox(
|
||||
error: snap.error,
|
||||
isError: true,
|
||||
maxHeight: 200,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
final r = snap.data!;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle_outline,
|
||||
size: 32,
|
||||
color: ChainColors.success,
|
||||
),
|
||||
const SizedBox(height: ChainSpace.md),
|
||||
Text(
|
||||
l.storeInstalledToast(r.name, r.version),
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
child: _buildBody(context, theme, l),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
|
@ -2936,6 +2947,68 @@ class _InstallProgressDialogState extends State<_InstallProgressDialog> {
|
|||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context, ThemeData theme, AppLocalizations l) {
|
||||
if (_error != null) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: theme.colorScheme.error, size: 32),
|
||||
const SizedBox(height: ChainSpace.md),
|
||||
ChainErrorBox(error: _error, isError: true, maxHeight: 200),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final done = _result != null;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (done) ...[
|
||||
Icon(Icons.check_circle_outline, size: 32, color: ChainColors.success),
|
||||
const SizedBox(height: ChainSpace.md),
|
||||
Text(
|
||||
l.storeInstalledToast(_result!.name, _result!.version),
|
||||
style: theme.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: ChainSpace.sm),
|
||||
ChainProgressBar(
|
||||
// Null until the hub can say how far along it is; the bar
|
||||
// runs indeterminate rather than sitting at zero.
|
||||
value: _step?.percent == null ? null : _step!.percent! / 100,
|
||||
busy: true,
|
||||
stage: _stageLabel(l),
|
||||
detail: _detail(l),
|
||||
),
|
||||
const SizedBox(height: ChainSpace.md),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l.storeInstallProgressBody,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
l.installElapsed(_elapsed.elapsed.inSeconds),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChainPillTone _toneForStatus(String s) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue