import 'package:flutter/material.dart'; import '../brew/models.dart'; import 'common.dart'; Future showPackageDetails( BuildContext context, String name, PackageKind kind, ) => showDialog( context: context, builder: (_) => PackageDetailsDialog(name: name, kind: kind), ); /// Full `brew info` for one package, with the actions that apply to it. class PackageDetailsDialog extends StatefulWidget { const PackageDetailsDialog({ super.key, required this.name, required this.kind, }); final String name; final PackageKind kind; @override State createState() => _PackageDetailsDialogState(); } class _PackageDetailsDialogState extends State { late Future _info; @override void initState() { super.initState(); _load(); } void _load() => _info = AppScope.read(context).brew.info(widget.name, widget.kind); Future _act(Future Function() action) async { await action(); if (mounted) setState(_load); } @override Widget build(BuildContext context) { return AlertDialog( contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0), content: SizedBox( width: 600, child: FutureBuilder( future: _info, builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return const SizedBox( height: 160, child: Center(child: CircularProgressIndicator()), ); } if (snap.hasError) return ErrorBanner(snap.error!); final p = snap.data; if (p == null) return Text('No information for ${widget.name}.'); return _Details(package: p); }, ), ), actions: [ FutureBuilder( future: _info, builder: (context, snap) => snap.data == null ? const SizedBox.shrink() : Wrap(spacing: 8, children: _actions(snap.data!)), ), TextButton( onPressed: () => Navigator.pop(context), child: const Text('Close'), ), ], ); } List _actions(Package p) { final brew = AppScope.read(context).brew; if (!p.isInstalled) { return [ FilledButton.icon( icon: const Icon(Icons.download), label: const Text('Install'), onPressed: () => _act(() => runTask(context, brew.install(p.fullName, p.kind))), ), ]; } return [ if (p.outdated && !p.pinned) FilledButton.icon( icon: const Icon(Icons.upgrade), label: const Text('Upgrade'), onPressed: () => _act( () => runTask(context, brew.upgrade(name: p.fullName, kind: p.kind)), ), ), if (p.kind == PackageKind.formula) OutlinedButton.icon( icon: Icon(p.pinned ? Icons.push_pin_outlined : Icons.push_pin), label: Text(p.pinned ? 'Unpin' : 'Pin'), onPressed: () => _act( () => runTask(context, brew.pin(p.fullName, pinned: !p.pinned)), ), ), OutlinedButton( onPressed: () => _act(() => runTask(context, brew.reinstall(p.fullName, p.kind))), child: const Text('Reinstall'), ), OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.error, ), icon: const Icon(Icons.delete_outline), label: const Text('Uninstall'), onPressed: () => _act(() async { final ok = await confirm( context, title: 'Uninstall ${p.name}?', message: p.kind == PackageKind.formula ? 'Homebrew will refuse if other installed formulae ' 'depend on it.' : 'This removes the app and its Homebrew-managed files.', action: 'Uninstall', ); if (!ok || !mounted) return false; return runTask(context, brew.uninstall(p.fullName, p.kind)); }), ), ]; } } class _Details extends StatelessWidget { const _Details({required this.package}); final Package package; @override Widget build(BuildContext context) { final p = package; final theme = Theme.of(context); final muted = TextStyle(color: theme.colorScheme.outline); return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Icon(kindIcon(p.kind)), const SizedBox(width: 8), Flexible( child: Text( p.displayName ?? p.name, style: theme.textTheme.headlineSmall, ), ), Tag(p.kind.label), if (p.isInstalled) Tag('installed', color: theme.colorScheme.primary), if (p.outdated) Tag('outdated', color: theme.colorScheme.tertiary), if (p.pinned) const Tag('pinned'), if (p.deprecated) Tag('deprecated', color: theme.colorScheme.error), ], ), if (p.displayName != null) Text(p.fullName, style: muted), const SizedBox(height: 12), if (p.desc != null) Text(p.desc!, style: theme.textTheme.bodyLarge), const SizedBox(height: 12), _field('Tap', p.tap ?? '—'), _field('Latest', p.latestVersion ?? '—'), _field('Installed', p.installedVersion ?? 'not installed'), if (p.homepage != null) Row( children: [ const SizedBox(width: 90, child: Text('Homepage')), Flexible( child: InkWell( onTap: () => openUrl(p.homepage!), child: Text( p.homepage!, style: TextStyle(color: theme.colorScheme.primary), ), ), ), ], ), if (p.dependencies.isNotEmpty) ...[ const SizedBox(height: 12), Text('Dependencies', style: theme.textTheme.titleSmall), const SizedBox(height: 6), Wrap( spacing: 6, runSpacing: 6, children: [ for (final d in p.dependencies) ActionChip( label: Text(d), onPressed: () => showPackageDetails(context, d, PackageKind.formula), ), ], ), ], if (p.caveats != null && p.caveats!.trim().isNotEmpty) ...[ const SizedBox(height: 12), Text('Caveats', style: theme.textTheme.titleSmall), const SizedBox(height: 6), SelectableText( p.caveats!.trim(), style: const TextStyle(fontFamily: 'Menlo', fontSize: 12), ), ], const SizedBox(height: 16), ], ), ); } Widget _field(String label, String value) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ SizedBox(width: 90, child: Text(label)), Flexible(child: SelectableText(value)), ], ), ); }