489 lines
16 KiB
Dart
489 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'providers/app_state.dart';
|
|
import 'screens/home_screen.dart';
|
|
import 'screens/beans_screen.dart';
|
|
import 'screens/machines_screen.dart';
|
|
import 'screens/recipes_screen.dart';
|
|
import 'screens/journal_screen.dart';
|
|
import 'screens/settings_screen.dart';
|
|
import 'components/global_search.dart';
|
|
|
|
void main() {
|
|
runApp(const CoffeeAtHomeApp());
|
|
}
|
|
|
|
class CoffeeAtHomeApp extends StatelessWidget {
|
|
const CoffeeAtHomeApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MultiProvider(
|
|
providers: [ChangeNotifierProvider(create: (_) => AppState())],
|
|
child: MaterialApp.router(
|
|
title: 'Coffee at Home',
|
|
theme: _buildTheme(),
|
|
routerConfig: _router,
|
|
),
|
|
);
|
|
}
|
|
|
|
ThemeData _buildTheme() {
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
colorScheme:
|
|
ColorScheme.fromSeed(
|
|
seedColor: const Color(0xFFD4A574), // Warm coffee brown
|
|
brightness: Brightness.dark,
|
|
).copyWith(
|
|
primary: const Color(0xFFD4A574),
|
|
secondary: const Color(0xFF6F4E37),
|
|
surface: const Color(0xFF2D2D2D),
|
|
onPrimary: const Color(0xFF1A1A1A),
|
|
onSecondary: const Color(0xFFFFFFFF),
|
|
onSurface: const Color(0xFFF5F5DC),
|
|
),
|
|
scaffoldBackgroundColor: const Color(0xFF1A1A1A),
|
|
cardTheme: CardThemeData(
|
|
color: const Color(0xFF2D2D2D),
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
|
side: const BorderSide(color: Color(0xFF3A3A3A)),
|
|
),
|
|
shadowColor: Colors.black.withValues(alpha: 0.4),
|
|
),
|
|
appBarTheme: const AppBarTheme(
|
|
backgroundColor: Color(0xFF2D2D2D),
|
|
foregroundColor: Color(0xFFF5F5DC),
|
|
elevation: 0,
|
|
titleTextStyle: TextStyle(
|
|
color: Color(0xFFF5F5DC),
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
surfaceTintColor: Colors.transparent,
|
|
),
|
|
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
|
backgroundColor: Color(0xFF2D2D2D),
|
|
selectedItemColor: Color(0xFFD4A574),
|
|
unselectedItemColor: Color(0xFFD2B48C),
|
|
type: BottomNavigationBarType.fixed,
|
|
elevation: 3,
|
|
),
|
|
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
|
backgroundColor: Color(0xFFD4A574),
|
|
foregroundColor: Color(0xFF1A1A1A),
|
|
),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFFD4A574),
|
|
foregroundColor: const Color(0xFF1A1A1A),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
textStyle: const TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: const Color(0xFFD4A574),
|
|
side: const BorderSide(color: Color(0xFFD4A574)),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
),
|
|
textTheme: const TextTheme(
|
|
headlineLarge: TextStyle(
|
|
color: Color(0xFFF5F5DC),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
headlineMedium: TextStyle(
|
|
color: Color(0xFFF5F5DC),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
headlineSmall: TextStyle(
|
|
color: Color(0xFFF5F5DC),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
titleLarge: TextStyle(
|
|
color: Color(0xFFF5F5DC),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
titleMedium: TextStyle(
|
|
color: Color(0xFFF5F5DC),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
titleSmall: TextStyle(
|
|
color: Color(0xFFF5F5DC),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
bodyLarge: TextStyle(color: Color(0xFFD2B48C)),
|
|
bodyMedium: TextStyle(color: Color(0xFFD2B48C)),
|
|
bodySmall: TextStyle(color: Color(0xFFD2B48C)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
final GoRouter _router = GoRouter(
|
|
routes: [
|
|
ShellRoute(
|
|
builder: (context, state, child) {
|
|
return MainScaffold(child: child);
|
|
},
|
|
routes: [
|
|
GoRoute(path: '/', redirect: (_, __) => '/home'),
|
|
GoRoute(path: '/home', builder: (context, state) => const HomeScreen()),
|
|
GoRoute(
|
|
path: '/beans',
|
|
builder: (context, state) => const BeansScreen(),
|
|
),
|
|
GoRoute(
|
|
path: '/machines',
|
|
builder: (context, state) => const MachinesScreen(),
|
|
),
|
|
GoRoute(
|
|
path: '/recipes',
|
|
builder: (context, state) => const RecipesScreen(),
|
|
),
|
|
GoRoute(
|
|
path: '/journal',
|
|
builder: (context, state) => const JournalScreen(),
|
|
),
|
|
GoRoute(
|
|
path: '/settings',
|
|
builder: (context, state) => const SettingsScreen(),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
|
|
class MainScaffold extends StatefulWidget {
|
|
final Widget child;
|
|
|
|
const MainScaffold({super.key, required this.child});
|
|
|
|
@override
|
|
State<MainScaffold> createState() => _MainScaffoldState();
|
|
}
|
|
|
|
class _MainScaffoldState extends State<MainScaffold> {
|
|
int _currentIndex = 0;
|
|
bool _isSearchOpen = false;
|
|
|
|
final List<String> _routes = [
|
|
'/home',
|
|
'/beans',
|
|
'/machines',
|
|
'/recipes',
|
|
'/journal',
|
|
'/settings',
|
|
];
|
|
|
|
void _onItemTapped(int index) {
|
|
setState(() {
|
|
_currentIndex = index;
|
|
});
|
|
context.go(_routes[index]);
|
|
}
|
|
|
|
void _openSearch() {
|
|
setState(() {
|
|
_isSearchOpen = true;
|
|
});
|
|
}
|
|
|
|
void _closeSearch() {
|
|
setState(() {
|
|
_isSearchOpen = false;
|
|
});
|
|
}
|
|
|
|
void _onSearchResultSelected(SearchResult result) {
|
|
switch (result.type) {
|
|
case 'Bean':
|
|
context.go('/beans');
|
|
break;
|
|
case 'Machine':
|
|
context.go('/machines');
|
|
break;
|
|
case 'Recipe':
|
|
context.go('/recipes');
|
|
break;
|
|
case 'Journal':
|
|
context.go('/journal');
|
|
break;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Update current index based on current route
|
|
final currentRoute = GoRouterState.of(context).uri.path;
|
|
final routeIndex = _routes.indexOf(currentRoute);
|
|
if (routeIndex != -1 && routeIndex != _currentIndex) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
setState(() {
|
|
_currentIndex = routeIndex;
|
|
});
|
|
});
|
|
}
|
|
|
|
return Scaffold(
|
|
body: _isSearchOpen
|
|
? Stack(
|
|
children: [
|
|
Column(
|
|
children: [
|
|
// AppBar
|
|
Container(
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF2D2D2D),
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: Color(0xFF3A3A3A),
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
child: SafeArea(
|
|
child: Container(
|
|
height: 56,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
children: [
|
|
const Expanded(
|
|
child: Text(
|
|
'Coffee at Home',
|
|
style: TextStyle(
|
|
color: Color(0xFFF5F5DC),
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.search,
|
|
color: Color(0xFFF5F5DC),
|
|
),
|
|
onPressed: _openSearch,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Body
|
|
Expanded(
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
return Container(
|
|
width: double.infinity,
|
|
constraints: BoxConstraints(
|
|
maxWidth: constraints.maxWidth > 1200
|
|
? 1200
|
|
: constraints.maxWidth,
|
|
),
|
|
margin: EdgeInsets.symmetric(
|
|
horizontal: constraints.maxWidth > 1200
|
|
? (constraints.maxWidth - 1200) / 2
|
|
: 0,
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
child: widget.child,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
// Bottom Navigation
|
|
Container(
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF2D2D2D),
|
|
border: Border(
|
|
top: BorderSide(color: Color(0xFF3A3A3A), width: 1),
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black26,
|
|
blurRadius: 8,
|
|
offset: Offset(0, -2),
|
|
),
|
|
],
|
|
),
|
|
child: SafeArea(
|
|
child: SizedBox(
|
|
height: 56,
|
|
child: Row(
|
|
children: [
|
|
_buildBottomNavItem(0, Icons.home, 'Home'),
|
|
_buildBottomNavItem(1, Icons.coffee, 'Beans'),
|
|
_buildBottomNavItem(
|
|
2,
|
|
Icons.kitchen,
|
|
'Equipment',
|
|
),
|
|
_buildBottomNavItem(
|
|
3,
|
|
Icons.menu_book,
|
|
'Recipes',
|
|
),
|
|
_buildBottomNavItem(4, Icons.book, 'Journal'),
|
|
_buildBottomNavItem(
|
|
5,
|
|
Icons.settings,
|
|
'Settings',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
GlobalSearchWidget(
|
|
isOpen: _isSearchOpen,
|
|
onClose: _closeSearch,
|
|
onResultSelected: _onSearchResultSelected,
|
|
),
|
|
],
|
|
)
|
|
: Column(
|
|
children: [
|
|
// AppBar
|
|
Container(
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF2D2D2D),
|
|
border: Border(
|
|
bottom: BorderSide(color: Color(0xFF3A3A3A), width: 1),
|
|
),
|
|
),
|
|
child: SafeArea(
|
|
child: Container(
|
|
height: 56,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
children: [
|
|
const Expanded(
|
|
child: Text(
|
|
'Coffee at Home',
|
|
style: TextStyle(
|
|
color: Color(0xFFF5F5DC),
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.search,
|
|
color: Color(0xFFF5F5DC),
|
|
),
|
|
onPressed: _openSearch,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Body
|
|
Expanded(
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
return Container(
|
|
width: double.infinity,
|
|
constraints: BoxConstraints(
|
|
maxWidth: constraints.maxWidth > 1200
|
|
? 1200
|
|
: constraints.maxWidth,
|
|
),
|
|
margin: EdgeInsets.symmetric(
|
|
horizontal: constraints.maxWidth > 1200
|
|
? (constraints.maxWidth - 1200) / 2
|
|
: 0,
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
child: widget.child,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
// Bottom Navigation
|
|
Container(
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF2D2D2D),
|
|
border: Border(
|
|
top: BorderSide(color: Color(0xFF3A3A3A), width: 1),
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black26,
|
|
blurRadius: 8,
|
|
offset: Offset(0, -2),
|
|
),
|
|
],
|
|
),
|
|
child: SafeArea(
|
|
child: SizedBox(
|
|
height: 56,
|
|
child: Row(
|
|
children: [
|
|
_buildBottomNavItem(0, Icons.home, 'Home'),
|
|
_buildBottomNavItem(1, Icons.coffee, 'Beans'),
|
|
_buildBottomNavItem(2, Icons.kitchen, 'Equipment'),
|
|
_buildBottomNavItem(3, Icons.menu_book, 'Recipes'),
|
|
_buildBottomNavItem(4, Icons.book, 'Journal'),
|
|
_buildBottomNavItem(5, Icons.settings, 'Settings'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBottomNavItem(int index, IconData icon, String label) {
|
|
final isSelected = _currentIndex == index;
|
|
return Expanded(
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () => _onItemTapped(index),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: isSelected
|
|
? const Color(0xFFD4A574)
|
|
: const Color(0xFFD2B48C),
|
|
size: 22,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: isSelected
|
|
? const Color(0xFFD4A574)
|
|
: const Color(0xFFD2B48C),
|
|
fontSize: 11,
|
|
fontWeight: isSelected
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|