From 5d511dcd99ec3c4f187a38bfc8640c2a3b79a6fe Mon Sep 17 00:00:00 2001 From: flemming-it Date: Wed, 1 Jul 2026 02:34:53 +0200 Subject: [PATCH] fix(studio): promote guided-setup button + remove dead ModulesPage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guided setup is the fastest path to a working first run, so lead with a FilledButton instead of a low-emphasis outlined one (welcome page). Delete pages/modules.dart — ModulesPage was never routed (superseded by the Store 'Installed' filter) and nothing imports it; flutter analyze stays clean. Signed-off-by: flemming-it --- lib/pages/modules.dart | 305 ----------------------------------------- lib/pages/welcome.dart | 5 +- 2 files changed, 4 insertions(+), 306 deletions(-) delete mode 100644 lib/pages/modules.dart diff --git a/lib/pages/modules.dart b/lib/pages/modules.dart deleted file mode 100644 index 24ccf40..0000000 --- a/lib/pages/modules.dart +++ /dev/null @@ -1,305 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../data/hub.dart'; -import '../l10n/app_localizations.dart'; -import '../theme/theme.dart'; -import '../theme/tokens.dart'; -import '../widgets/widgets.dart'; - -class ModulesPage extends StatefulWidget { - const ModulesPage({super.key}); - - @override - State createState() => _ModulesPageState(); -} - -class _ModulesPageState extends State { - late Future> _future; - late Future> _historyFuture; - - @override - void initState() { - super.initState(); - _future = HubService.instance.listModules(); - _historyFuture = _loadHistory(); - } - - Future> _loadHistory() { - return HubService.instance.recentEvents( - limit: 10, - types: const ['module.installed', 'module.uninstalled'], - ); - } - - void _refresh() => setState(() { - _future = HubService.instance.listModules(); - _historyFuture = _loadHistory(); - }); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.modulesTitle), - actions: [ - IconButton( - icon: const Icon(Icons.refresh, size: 18), - tooltip: AppLocalizations.of(context)!.modulesReloadTooltip, - onPressed: _refresh, - ), - const SizedBox(width: ChainSpace.sm), - ], - ), - body: FutureBuilder>( - future: _future, - builder: (context, snapshot) { - final l = AppLocalizations.of(context)!; - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - if (snapshot.hasError) { - return ChainEmptyState( - icon: Icons.cloud_off_outlined, - iconColor: Theme.of(context).colorScheme.error, - title: l.hubUnreachable, - hint: l.hubUnreachableHint, - action: FilledButton.tonal( - onPressed: _refresh, - child: Text(l.buttonRetry), - ), - ); - } - final modules = snapshot.data ?? []; - if (modules.isEmpty) { - return ChainEmptyState( - icon: Icons.extension_outlined, - title: l.modulesNoneTitle, - hint: l.modulesNoneHint, - ); - } - return ListView( - padding: const EdgeInsets.all(ChainSpace.xl), - children: [ - _RecentActivityPanel(future: _historyFuture), - const SizedBox(height: ChainSpace.md), - for (var i = 0; i < modules.length; i++) ...[ - if (i > 0) const SizedBox(height: ChainSpace.md), - GestureDetector( - onTap: () async { - final uninstalled = await ChainModuleSheet.show( - context, - modules[i].name, - ); - if (uninstalled) _refresh(); - }, - behavior: HitTestBehavior.opaque, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: _ModuleCard(module: modules[i]), - ), - ), - ], - ], - ); - }, - ), - ); - } -} - -class _ModuleCard extends StatelessWidget { - final ModuleSummary module; - - const _ModuleCard({required this.module}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return ChainCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - module.name, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: ChainSpace.sm), - ChainPill( - label: 'v${module.version}', - tone: ChainPillTone.neutral, - monospace: true, - ), - ], - ), - const SizedBox(height: ChainSpace.md), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 110, - child: Text( - AppLocalizations.of(context)!.modulesCapabilitiesLabel, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - letterSpacing: 0.4, - ), - ), - ), - Expanded( - child: Wrap( - spacing: ChainSpace.xs, - runSpacing: ChainSpace.xs, - children: module.capabilities - .map( - (c) => ChainPill( - label: c, - tone: ChainPillTone.accent, - monospace: true, - ), - ) - .toList(), - ), - ), - ], - ), - ], - ), - ); - } -} - -/// Compact "Recent activity" header listing the last few -/// install / uninstall events from the audit log. Lets -/// operators trace "wait, when did that module appear?" -/// without leaving the Modules page. Hidden when the audit -/// log has no relevant events yet. -class _RecentActivityPanel extends StatelessWidget { - final Future> future; - const _RecentActivityPanel({required this.future}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return FutureBuilder>( - future: future, - builder: (context, snap) { - final events = snap.data ?? const []; - if (events.isEmpty) return const SizedBox.shrink(); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: ChainSpace.md, - vertical: ChainSpace.sm, - ), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(ChainRadius.sm), - border: Border.all(color: theme.colorScheme.outlineVariant), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.history, - size: 14, - color: theme.colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 6), - Text( - AppLocalizations.of(context)!.modulesRecentActivity, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - letterSpacing: 0.6, - fontSize: 10, - ), - ), - const SizedBox(width: ChainSpace.sm), - Text( - AppLocalizations.of( - context, - )!.modulesRecentActivityHint(events.length), - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ), - const SizedBox(height: ChainSpace.xs), - for (final e in events) _ActivityRow(event: e), - ], - ), - ); - }, - ); - } -} - -class _ActivityRow extends StatelessWidget { - final AuditEvent event; - const _ActivityRow({required this.event}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final installed = event.type == 'module.installed'; - final accent = installed ? ChainColors.success : ChainColors.warning; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - children: [ - Icon( - installed ? Icons.add_circle_outline : Icons.remove_circle_outline, - size: 14, - color: accent, - ), - const SizedBox(width: 6), - SizedBox( - width: 150, - child: Text( - _formatTimestamp(event.timestamp.toLocal()), - style: ChainTheme.mono( - size: 10, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - Expanded( - child: Text( - '${installed ? AppLocalizations.of(context)!.modulesActivityInstalled : AppLocalizations.of(context)!.modulesActivityUninstalled} ' - '${event.moduleName ?? "(unknown)"}' - '${event.moduleVersion != null ? " v${event.moduleVersion}" : ""}', - style: theme.textTheme.bodySmall, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - } -} - -/// Locale-unambiguous timestamp shared by the activity rows. -/// Same-day events get HH:mm:ss; older events get the full -/// YYYY-MM-DD HH:mm:ss so an operator never wonders what day -/// "21:25" was. -String _formatTimestamp(DateTime local) { - final now = DateTime.now(); - final hh = local.hour.toString().padLeft(2, '0'); - final mm = local.minute.toString().padLeft(2, '0'); - final ss = local.second.toString().padLeft(2, '0'); - final time = '$hh:$mm:$ss'; - final sameDay = - local.year == now.year && - local.month == now.month && - local.day == now.day; - if (sameDay) return time; - final mo = local.month.toString().padLeft(2, '0'); - final dd = local.day.toString().padLeft(2, '0'); - return '${local.year}-$mo-$dd $time'; -} diff --git a/lib/pages/welcome.dart b/lib/pages/welcome.dart index bffebee..144b52f 100644 --- a/lib/pages/welcome.dart +++ b/lib/pages/welcome.dart @@ -63,7 +63,10 @@ class WelcomePage extends StatelessWidget { const SizedBox(height: ChainSpace.md), Align( alignment: Alignment.centerLeft, - child: OutlinedButton.icon( + // High-emphasis: the guided setup is the fastest path + // to a working first run, so lead with a filled button + // rather than a low-key outlined one. + child: FilledButton.icon( onPressed: () => GuidedSetupDialog.show(context), icon: const Icon(Icons.auto_fix_high), label: Text(AppLocalizations.of(context)!.guidedSetupTitle),