import 'dart:io'; import 'package:flutter/material.dart'; import 'brew/brew_client.dart'; import 'state/app_state.dart'; import 'ui/common.dart'; import 'ui/installed_page.dart'; import 'ui/outdated_page.dart'; import 'ui/search_page.dart'; import 'ui/taps_page.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); AppState? state; final brewPath = BrewClient.locate(); if (brewPath != null) { state = AppState( BrewClient(brewPath: brewPath, askpassPath: _writeAskpass()), ); state.refresh(); } runApp(TaproomApp(state: state)); } /// Writes a SUDO_ASKPASS helper that asks for the password with a native /// dialog, for casks whose installers need sudo. Returns null if it can't. String? _writeAskpass() { try { final f = File('${Directory.systemTemp.path}/taproom-askpass.sh'); f.writeAsStringSync('''#!/bin/sh exec /usr/bin/osascript -e 'text returned of (display dialog "Homebrew needs your password to continue." default answer "" with hidden answer with title "Taproom" with icon caution)' '''); Process.runSync('chmod', ['700', f.path]); return f.path; } catch (_) { return null; } } class TaproomApp extends StatelessWidget { const TaproomApp({super.key, required this.state}); /// Null when Homebrew isn't installed. final AppState? state; @override Widget build(BuildContext context) { ThemeData theme(Brightness b) => ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFFE0A030), brightness: b, ), ); final app = MaterialApp( title: 'Taproom', debugShowCheckedModeBanner: false, theme: theme(Brightness.light), darkTheme: theme(Brightness.dark), home: state == null ? const _NoBrew() : const HomeScreen(), ); // Above MaterialApp so dialogs (which live in the root overlay) see it. return state == null ? app : AppScope(state: state!, child: app); } } class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { int _index = 0; static const _pages = [ InstalledPage(), OutdatedPage(), SearchPage(), TapsPage(), ]; @override Widget build(BuildContext context) { final state = AppScope.of(context); final outdated = state.outdated.length; return Scaffold( body: Row( children: [ NavigationRail( selectedIndex: _index, onDestinationSelected: (i) => setState(() => _index = i), labelType: NavigationRailLabelType.all, leading: const Padding( padding: EdgeInsets.symmetric(vertical: 12), child: Icon(Icons.sports_bar, size: 32), ), destinations: [ const NavigationRailDestination( icon: Icon(Icons.inventory_2_outlined), selectedIcon: Icon(Icons.inventory_2), label: Text('Installed'), ), NavigationRailDestination( icon: Badge( isLabelVisible: outdated > 0, label: Text('$outdated'), child: const Icon(Icons.update), ), label: const Text('Outdated'), ), const NavigationRailDestination( icon: Icon(Icons.search), label: Text('Search'), ), const NavigationRailDestination( icon: Icon(Icons.water_drop_outlined), selectedIcon: Icon(Icons.water_drop), label: Text('Taps'), ), ], trailing: Expanded( child: Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ IconButton( tooltip: 'brew update — fetch the latest package index', icon: const Icon(Icons.cloud_sync_outlined), onPressed: () => runTask(context, state.brew.update()), ), IconButton( tooltip: 'brew cleanup — remove old versions and caches', icon: const Icon(Icons.cleaning_services_outlined), onPressed: () => runTask(context, state.brew.cleanup()), ), ], ), ), ), ), ), const VerticalDivider(width: 1), Expanded( child: IndexedStack(index: _index, children: _pages), ), ], ), ); } } class _NoBrew extends StatelessWidget { const _NoBrew(); @override Widget build(BuildContext context) => Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.sports_bar, size: 64), const SizedBox(height: 16), Text( 'Homebrew not found', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 8), const Text('Looked in /opt/homebrew and /usr/local.'), const SizedBox(height: 16), FilledButton( onPressed: () => openUrl('https://brew.sh'), child: const Text('Get Homebrew'), ), ], ), ), ); }