taproom/lib/ui/search_page.dart
Philip Peterson 24dcfcf70d Taproom: a Flutter desktop frontend for Homebrew
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>
2026-10-10 20:12:21 -08:00

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),
);
},
);
},
),
),
],
);
}
}