import 'package:flutter/material.dart'; import '../data/hub.dart'; class ModulesPage extends StatefulWidget { const ModulesPage({super.key}); @override State createState() => _ModulesPageState(); } class _ModulesPageState extends State { late Future> _future; @override void initState() { super.initState(); _future = HubService.instance.listModules(); } void _refresh() => setState(() { _future = HubService.instance.listModules(); }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text('Installed Modules'), centerTitle: false, actions: [ IconButton( icon: const Icon(Icons.refresh), tooltip: 'Reload', onPressed: _refresh, ), ], ), body: FutureBuilder>( future: _future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return _ConnectionError( message: 'Hub unreachable: ${snapshot.error}', onRetry: _refresh, ); } final modules = snapshot.data ?? []; if (modules.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Text( 'No modules installed.\n' 'Run `fai install ` or check ~/.fai/modules/.', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), ); } return ListView.separated( padding: const EdgeInsets.all(24), itemCount: modules.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (context, i) { final m = modules[i]; return Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: theme.colorScheme.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( m.name, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: theme.colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(4), ), child: Text( 'v${m.version}', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSecondaryContainer, ), ), ), ], ), const SizedBox(height: 8), _ChipRow(label: 'Capabilities', items: m.capabilities), ], ), ), ); }, ); }, ), ); } } class _ChipRow extends StatelessWidget { final String label; final List items; const _ChipRow({required this.label, required this.items}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 140, child: Text( label, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), Expanded( child: Wrap( spacing: 6, runSpacing: 6, children: items .map( (s) => Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: Text( s, style: theme.textTheme.bodySmall?.copyWith( fontFamily: 'monospace', ), ), ), ) .toList(), ), ), ], ); } } class _ConnectionError extends StatelessWidget { final String message; final VoidCallback onRetry; const _ConnectionError({required this.message, required this.onRetry}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.cloud_off_outlined, size: 48, color: theme.colorScheme.error, ), const SizedBox(height: 16), Text( message, textAlign: TextAlign.center, style: theme.textTheme.bodyMedium, ), const SizedBox(height: 8), Text( 'Start the hub with `fai serve`.', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 16), FilledButton.tonal( onPressed: onRetry, child: const Text('Retry'), ), ], ), ), ); } }