fix(studio): live-search exception, daemon-card width, featured tile size; add Start-hub button (v0.20.1)

Three reported bugs and one onramp gap:

- Store live-search threw `setState() callback returned a
  Future`. The `=>` body of `_runSearch` evaluated to the
  Future returned by `_load()`. Switched to a block body so
  the closure resolves to void; the future itself is still
  awaited by the FutureBuilder.

- Doctor's Daemon-control card was visibly narrower than its
  siblings — its only stretching child was a Wrap of small
  buttons. Wrapped the content in `SizedBox(width: infinity)`
  so the card fills the section's width like every other
  panel. Card now leads with the live `running on local:
  127.0.0.1:50051` status row + status pill so operators see
  state at a glance, then offers Restart / Start (when
  stopped) / Stop / Status.

- Featured tiles were 320×152 against the regular grid's
  ~360×168, so the editorial picks looked subordinate to
  the dense grid. Bumped to 480×220 with bigger icon, larger
  title, larger tagline so the strip reads as a hero band.

- ConnectionPill in the sidebar now offers an inline "Start
  hub" button when the daemon is unreachable. Spawns
  `fai daemon start` via SystemActions; the shell-level
  health poll picks the daemon up on its next tick. Closes
  the chicken-and-egg for Windows users who can't open a
  shell to bootstrap.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-07 21:27:09 +02:00
parent 26691b7471
commit fb7352d182
4 changed files with 256 additions and 96 deletions

View file

@ -64,7 +64,15 @@ class _StorePageState extends State<StorePage> {
return _installedOnly ? all.where((e) => e.installed).toList() : all;
}
void _runSearch() => setState(() => _future = _load());
void _runSearch() {
// Block body, so the closure's return type is `void`
// rather than `Future`. setState() rejects closures that
// resolve to a Future the assignment must be synchronous
// even though the future itself is awaited later.
setState(() {
_future = _load();
});
}
Future<void> _openDetail(StoreItem item) async {
final changed = await _StoreDetailSheet.show(context, item, _locale);
@ -533,7 +541,11 @@ class _FeaturedStrip extends StatelessWidget {
),
const SizedBox(height: FaiSpace.sm),
SizedBox(
height: 152,
// Tiles render larger than regular grid cards
// (which are 168 × ~360) so the editorial picks
// visibly stand out. Same hierarchy you see on app
// stores: hero band before the dense list.
height: 220,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: items.length,
@ -541,7 +553,7 @@ class _FeaturedStrip extends StatelessWidget {
itemBuilder: (context, i) {
final item = items[i];
return SizedBox(
width: 320,
width: 480,
child: _FeaturedTile(
item: item,
locale: locale,
@ -584,12 +596,12 @@ class _FeaturedTile extends StatelessWidget {
onTap: onTap,
borderRadius: BorderRadius.circular(FaiRadius.md),
child: Container(
padding: const EdgeInsets.all(FaiSpace.md),
padding: const EdgeInsets.all(FaiSpace.lg),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(FaiRadius.md),
gradient: LinearGradient(
colors: [
theme.colorScheme.primary.withValues(alpha: 0.10),
theme.colorScheme.primary.withValues(alpha: 0.14),
theme.colorScheme.surfaceContainer.withValues(alpha: 0.0),
],
begin: Alignment.topLeft,
@ -602,34 +614,46 @@ class _FeaturedTile extends StatelessWidget {
Row(
children: [
CircleAvatar(
radius: 20,
radius: 24,
backgroundColor:
theme.colorScheme.primary.withValues(alpha: 0.16),
theme.colorScheme.primary.withValues(alpha: 0.18),
child: Icon(
_iconForCategory(item.category),
size: 22,
size: 26,
color: theme.colorScheme.primary,
),
),
const SizedBox(width: FaiSpace.sm),
const SizedBox(width: FaiSpace.md),
Expanded(
child: Text(
item.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.name,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (item.category.isNotEmpty)
Text(
item.category,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
Icon(Icons.star, size: 14, color: FaiColors.success),
Icon(Icons.star, size: 18, color: FaiColors.success),
],
),
const SizedBox(height: FaiSpace.sm),
const SizedBox(height: FaiSpace.md),
Expanded(
child: Text(
tagline.isEmpty ? '(no tagline)' : tagline,
style: theme.textTheme.bodyMedium,
style: theme.textTheme.bodyLarge,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
@ -659,11 +683,8 @@ class _FeaturedTile extends StatelessWidget {
else if (installable)
FilledButton.icon(
onPressed: onInstall,
icon: const Icon(Icons.download, size: 14),
icon: const Icon(Icons.download, size: 16),
label: const Text('Install'),
style: FilledButton.styleFrom(
visualDensity: VisualDensity.compact,
),
),
],
),