import 'package:flutter/material.dart';
import '../brew/models.dart';
import 'common.dart';
Future<void> showPackageDetails(
BuildContext context,
String name,
PackageKind kind,
) => showDialog<void>(
context: context,
builder: (_) => PackageDetailsDialog(name: name, kind: kind),
);
class PackageDetailsDialog extends StatefulWidget {
const PackageDetailsDialog({
super.key,
required this.name,
required this.kind,
});
final String name;
final PackageKind kind;
@override
State<PackageDetailsDialog> createState() => _PackageDetailsDialogState();
}
class _PackageDetailsDialogState extends State<PackageDetailsDialog> {
late Future<Package?> _info;
@override
void initState() {
super.initState();
_load();
}
void _load() =>
_info = AppScope.read(context).brew.info(widget.name, widget.kind);
Future<void> _act(Future<bool> 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<Package?>(
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<Package?>(
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<Widget> _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)),
],
),
);
}