Browse installed packages, upgrade outdated ones, search formulae and casks, and explore taps, all by driving the brew CLI. Includes a Taskfile for running, testing and building. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
4.1 KiB
Dart
125 lines
4.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../brew/models.dart';
|
|
import 'common.dart';
|
|
import 'package_details.dart';
|
|
|
|
class SearchPage extends StatefulWidget {
|
|
const SearchPage({super.key});
|
|
|
|
@override
|
|
State<SearchPage> createState() => _SearchPageState();
|
|
}
|
|
|
|
class _SearchPageState extends State<SearchPage> {
|
|
final _controller = TextEditingController();
|
|
Future<List<SearchResult>>? _results;
|
|
String _query = '';
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _search() {
|
|
final q = _controller.text.trim();
|
|
if (q.isEmpty) return;
|
|
setState(() {
|
|
_query = q;
|
|
_results = AppScope.read(context).brew.search(q);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final state = AppScope.of(context);
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
PageHeader(
|
|
title: 'Search',
|
|
bottom: Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _controller,
|
|
autofocus: true,
|
|
onSubmitted: (_) => _search(),
|
|
decoration: const InputDecoration(
|
|
isDense: true,
|
|
prefixIcon: Icon(Icons.search),
|
|
hintText: 'Formula or cask name — /regex/ also works',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
FilledButton(onPressed: _search, child: const Text('Search')),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _results == null
|
|
? const EmptyState(
|
|
'Search all formulae and casks in your taps.',
|
|
icon: Icons.search,
|
|
)
|
|
: FutureBuilder<List<SearchResult>>(
|
|
future: _results,
|
|
builder: (context, snap) {
|
|
if (snap.connectionState != ConnectionState.done) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
if (snap.hasError) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: ErrorBanner(snap.error!),
|
|
);
|
|
}
|
|
final results = snap.data!;
|
|
if (results.isEmpty) {
|
|
return EmptyState('No matches for "$_query".');
|
|
}
|
|
return ListView.builder(
|
|
itemCount: results.length,
|
|
itemBuilder: (context, i) {
|
|
final r = results[i];
|
|
final installed = state.installedPackage(
|
|
r.name,
|
|
r.kind,
|
|
);
|
|
return ListTile(
|
|
leading: Icon(kindIcon(r.kind)),
|
|
title: Row(
|
|
children: [
|
|
Text(r.name),
|
|
Tag(r.kind.label),
|
|
if (installed != null)
|
|
Tag(
|
|
'installed',
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
],
|
|
),
|
|
trailing: installed != null
|
|
? null
|
|
: OutlinedButton(
|
|
onPressed: () => runTask(
|
|
context,
|
|
state.brew.install(r.name, r.kind),
|
|
),
|
|
child: const Text('Install'),
|
|
),
|
|
onTap: () =>
|
|
showPackageDetails(context, r.name, r.kind),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|