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:
flemming-it 2026-09-08 14:00:24 +02:00
parent 2bc556e0f5
commit 1549334767
9 changed files with 490 additions and 74 deletions

View file

@ -22,6 +22,7 @@ import '../data/system_actions.dart';
import '../l10n/app_localizations.dart';
import '../main.dart' show StudioShellState;
import '../theme/tokens.dart';
import 'chain_progress_bar.dart';
import 'chain_stores_dialog.dart';
/// Allowed wire values per answer an AI suggestion is validated
@ -570,17 +571,43 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
/// Install one plan module by capability name the hub resolves
/// the bundle URL from its store index.
/// Latest progress observation per module being installed.
final Map<String, InstallStep> _installSteps = {};
Future<void> _install(String module) async {
setState(() => _installing.add(module));
try {
await HubService.instance.installModule(source: module);
// Streamed, not awaited blind: a module bundle can take a while
// on a slow line, and a dead-looking button is what made people
// click twice.
await for (final step
in HubService.instance.installModuleStreaming(source: module)) {
if (mounted) setState(() => _installSteps[module] = step);
}
if (mounted) setState(() => _installed.add(module));
} catch (e) {
if (mounted) {
await showChainErrorDialog(context, 'install $module', e);
}
} finally {
if (mounted) setState(() => _installing.remove(module));
if (mounted) {
setState(() {
_installing.remove(module);
_installSteps.remove(module);
});
}
}
}
/// Plain-language label for a module's current install phase.
String _installStage(AppLocalizations l, String module) {
switch (_installSteps[module]?.phase) {
case 'download':
return l.installPhaseDownload;
case 'install':
return l.installPhaseInstall;
default:
return l.installPhaseResolve;
}
}
@ -966,18 +993,39 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
_installed.contains(m)
? _doneRow(l.setupActionInstalled(m))
: _actionRow(
FilledButton.tonalIcon(
onPressed: (!_hubUp || _installing.contains(m))
? null
: () => _install(m),
icon: _installing.contains(m)
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.download, size: 18),
label: Text(l.setupActionInstall(m)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FilledButton.tonalIcon(
onPressed: (!_hubUp || _installing.contains(m))
? null
: () => _install(m),
icon: _installing.contains(m)
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.download, size: 18),
label: Text(l.setupActionInstall(m)),
),
if (_installing.contains(m)) ...[
const SizedBox(height: ChainSpace.sm),
SizedBox(
width: 320,
child: ChainProgressBar(
value: _installSteps[m]?.percent == null
? null
: _installSteps[m]!.percent! / 100,
stage: _installStage(l, m),
height: 6,
),
),
],
],
),
),
],