// Module-store manager — list the configured module stores (+ the // bundled seed), add a store by index URL, or remove one. The hub merges // every store's modules into one searchable index, so adding a store // (e.g. a domain app's published modules) makes them appear in the Store. import 'package:chain_client_sdk/chain_client_sdk.dart'; import 'package:flutter/material.dart'; import '../data/error_presentation.dart'; import '../data/hub.dart'; import '../theme/tokens.dart'; class ChainStoresDialog extends StatefulWidget { const ChainStoresDialog._(); static Future show(BuildContext context) => showDialog( context: context, builder: (_) => const ChainStoresDialog._(), ); @override State createState() => _ChainStoresDialogState(); } class _ChainStoresDialogState extends State { late Future> _future; final _name = TextEditingController(); final _url = TextEditingController(); bool _busy = false; @override void initState() { super.initState(); _reload(); } void _reload() => setState(() => _future = HubService.instance.listStores()); @override void dispose() { _name.dispose(); _url.dispose(); super.dispose(); } Future _add() async { final name = _name.text.trim(); final url = _url.text.trim(); if (name.isEmpty || url.isEmpty) return; setState(() => _busy = true); try { final r = await HubService.instance.addStore(name: name, url: url); if (!mounted) return; _name.clear(); _url.clear(); ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(r.message))); _reload(); } catch (e) { if (mounted) { showFaiErrorSnack(context, 'store.add', e, title: 'Could not add store'); } } finally { if (mounted) setState(() => _busy = false); } } Future _remove(String name) async { try { final r = await HubService.instance.removeStore(name); if (!mounted) return; ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(r.message))); _reload(); } catch (e) { if (mounted) { showFaiErrorSnack(context, 'store.remove', e, title: 'Could not remove store'); } } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return AlertDialog( title: const Text('Module stores'), content: SizedBox( width: 540, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'The hub merges modules from every store below into one index. ' 'Add a store index URL to make its modules installable here.', style: theme.textTheme.bodySmall ?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), const SizedBox(height: ChainSpace.md), Flexible( child: FutureBuilder>( future: _future, builder: (c, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } final stores = snap.data ?? const []; return ListView( shrinkWrap: true, children: [ for (final s in stores) ListTile( dense: true, leading: Icon( s.removable ? Icons.cloud_outlined : Icons.inventory_2_outlined, size: 18, ), title: Text(s.name), subtitle: Text( s.url.isEmpty ? '${s.moduleCount} modules · bundled' : '${s.url}\n${s.moduleCount} modules', ), isThreeLine: s.url.isNotEmpty, trailing: s.removable ? IconButton( icon: const Icon(Icons.delete_outline), tooltip: 'Remove', onPressed: () => _remove(s.name), ) : const Icon(Icons.lock_outline, size: 16), ), ], ); }, ), ), const Divider(), Text('Add a store', style: theme.textTheme.titleSmall), const SizedBox(height: ChainSpace.sm), Row( children: [ Expanded( flex: 2, child: TextField( controller: _name, decoration: const InputDecoration( labelText: 'Name', isDense: true, ), ), ), const SizedBox(width: ChainSpace.sm), Expanded( flex: 4, child: TextField( controller: _url, decoration: const InputDecoration( labelText: 'Index URL (https:// or file://)', isDense: true, ), onSubmitted: (_) => _add(), ), ), ], ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Close'), ), FilledButton( onPressed: _busy ? null : _add, child: _busy ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Add store'), ), ], ); } }