home_screen.dart 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import 'package:flutter/material.dart';
  2. import '../services/database_helper.dart';
  3. import '../theme/theme_controller.dart';
  4. import '../widgets/brand_mark.dart';
  5. import 'analysis_screen.dart';
  6. import 'live_analysis_screen.dart';
  7. import 'static_capture_screen.dart';
  8. import 'history_screen.dart';
  9. class HomeScreen extends StatefulWidget {
  10. const HomeScreen({super.key});
  11. @override
  12. State<HomeScreen> createState() => _HomeScreenState();
  13. }
  14. class _HomeScreenState extends State<HomeScreen> {
  15. int? _totalScans;
  16. @override
  17. void initState() {
  18. super.initState();
  19. _loadStats();
  20. }
  21. Future<void> _loadStats() async {
  22. final records = await DatabaseHelper().getAllRecords();
  23. if (mounted) setState(() => _totalScans = records.length);
  24. }
  25. @override
  26. Widget build(BuildContext context) {
  27. final colorScheme = Theme.of(context).colorScheme;
  28. return Scaffold(
  29. body: SafeArea(
  30. child: SingleChildScrollView(
  31. padding: const EdgeInsets.all(20),
  32. child: Column(
  33. crossAxisAlignment: CrossAxisAlignment.start,
  34. children: [
  35. Row(
  36. children: [
  37. const BrandMark(size: 44),
  38. const SizedBox(width: 12),
  39. Expanded(
  40. child: Text(
  41. 'Palm Oil Ripeness AI',
  42. style: TextStyle(
  43. fontSize: 20,
  44. fontWeight: FontWeight.bold,
  45. color: colorScheme.onSurface,
  46. ),
  47. ),
  48. ),
  49. ValueListenableBuilder<ThemeMode>(
  50. valueListenable: ThemeController.themeMode,
  51. builder: (context, mode, _) {
  52. return IconButton(
  53. icon: Icon(_themeModeIcon(mode), color: colorScheme.onSurface),
  54. tooltip: 'Theme',
  55. onPressed: () => _showThemePicker(context, mode),
  56. );
  57. },
  58. ),
  59. ],
  60. ),
  61. const SizedBox(height: 20),
  62. _buildStatCard(colorScheme),
  63. const SizedBox(height: 24),
  64. GridView.count(
  65. shrinkWrap: true,
  66. physics: const NeverScrollableScrollPhysics(),
  67. crossAxisCount: 2,
  68. mainAxisSpacing: 16,
  69. crossAxisSpacing: 16,
  70. childAspectRatio: 0.95,
  71. children: [
  72. _buildTile(
  73. context,
  74. title: 'Analyze Gallery',
  75. subtitle: 'Detect ripeness from photos',
  76. icon: Icons.photo_library,
  77. background: colorScheme.primary,
  78. foreground: colorScheme.onPrimary,
  79. onTap: () => Navigator.push(
  80. context,
  81. MaterialPageRoute(builder: (context) => const AnalysisScreen()),
  82. ),
  83. ),
  84. _buildTile(
  85. context,
  86. title: 'Snap & Analyze',
  87. subtitle: 'Manual high-res capture',
  88. icon: Icons.camera_alt,
  89. background: colorScheme.secondary,
  90. foreground: colorScheme.onSecondary,
  91. onTap: () => Navigator.push(
  92. context,
  93. MaterialPageRoute(builder: (context) => const StaticCaptureScreen()),
  94. ),
  95. ),
  96. _buildTile(
  97. context,
  98. title: 'Live Inference',
  99. subtitle: 'Real-time "Point-and-Scan"',
  100. icon: Icons.camera,
  101. background: colorScheme.primaryContainer,
  102. foreground: colorScheme.onPrimaryContainer,
  103. onTap: () => Navigator.push(
  104. context,
  105. MaterialPageRoute(builder: (context) => const LiveAnalysisScreen()),
  106. ),
  107. ),
  108. _buildTile(
  109. context,
  110. title: 'History Vault',
  111. subtitle: 'View previous detections',
  112. icon: Icons.history,
  113. background: colorScheme.surfaceContainerHighest,
  114. foreground: colorScheme.onSurface,
  115. bordered: true,
  116. onTap: () => Navigator.push(
  117. context,
  118. MaterialPageRoute(builder: (context) => const HistoryScreen()),
  119. ),
  120. ),
  121. ],
  122. ),
  123. ],
  124. ),
  125. ),
  126. ),
  127. );
  128. }
  129. IconData _themeModeIcon(ThemeMode mode) {
  130. switch (mode) {
  131. case ThemeMode.light:
  132. return Icons.light_mode;
  133. case ThemeMode.dark:
  134. return Icons.dark_mode;
  135. case ThemeMode.system:
  136. return Icons.brightness_auto;
  137. }
  138. }
  139. Future<void> _showThemePicker(BuildContext context, ThemeMode current) async {
  140. await showModalBottomSheet(
  141. context: context,
  142. shape: const RoundedRectangleBorder(
  143. borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
  144. ),
  145. builder: (sheetContext) {
  146. return SafeArea(
  147. child: Column(
  148. mainAxisSize: MainAxisSize.min,
  149. children: [
  150. const Padding(
  151. padding: EdgeInsets.fromLTRB(20, 20, 20, 8),
  152. child: Align(
  153. alignment: Alignment.centerLeft,
  154. child: Text("Theme", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
  155. ),
  156. ),
  157. _themeOptionTile(sheetContext, "Light", Icons.light_mode, ThemeMode.light, current),
  158. _themeOptionTile(sheetContext, "Dark", Icons.dark_mode, ThemeMode.dark, current),
  159. _themeOptionTile(sheetContext, "System", Icons.brightness_auto, ThemeMode.system, current),
  160. const SizedBox(height: 8),
  161. ],
  162. ),
  163. );
  164. },
  165. );
  166. }
  167. Widget _themeOptionTile(BuildContext context, String label, IconData icon, ThemeMode mode, ThemeMode current) {
  168. final selected = mode == current;
  169. return ListTile(
  170. leading: Icon(icon),
  171. title: Text(label),
  172. trailing: selected ? const Icon(Icons.check) : null,
  173. onTap: () {
  174. ThemeController.setThemeMode(mode);
  175. Navigator.pop(context);
  176. },
  177. );
  178. }
  179. Widget _buildStatCard(ColorScheme colorScheme) {
  180. return Container(
  181. width: double.infinity,
  182. padding: const EdgeInsets.all(16),
  183. decoration: BoxDecoration(
  184. color: colorScheme.surfaceContainerHighest,
  185. borderRadius: BorderRadius.circular(16),
  186. border: Border.all(color: colorScheme.primary.withOpacity(0.15)),
  187. ),
  188. child: Row(
  189. children: [
  190. Icon(Icons.analytics_outlined, color: colorScheme.primary, size: 28),
  191. const SizedBox(width: 12),
  192. Column(
  193. crossAxisAlignment: CrossAxisAlignment.start,
  194. children: [
  195. Text(
  196. _totalScans == null ? '—' : '$_totalScans',
  197. style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: colorScheme.onSurface),
  198. ),
  199. Text(
  200. 'Total Scans',
  201. style: TextStyle(fontSize: 13, color: colorScheme.onSurface.withOpacity(0.6)),
  202. ),
  203. ],
  204. ),
  205. ],
  206. ),
  207. );
  208. }
  209. Widget _buildTile(
  210. BuildContext context, {
  211. required String title,
  212. required String subtitle,
  213. required IconData icon,
  214. required Color background,
  215. required Color foreground,
  216. required VoidCallback onTap,
  217. bool bordered = false,
  218. }) {
  219. return GestureDetector(
  220. onTap: onTap,
  221. child: Container(
  222. padding: const EdgeInsets.all(16),
  223. decoration: BoxDecoration(
  224. color: background,
  225. borderRadius: BorderRadius.circular(20),
  226. border: bordered ? Border.all(color: foreground.withOpacity(0.15)) : null,
  227. ),
  228. child: Column(
  229. crossAxisAlignment: CrossAxisAlignment.start,
  230. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  231. children: [
  232. Icon(icon, color: foreground, size: 32),
  233. Column(
  234. crossAxisAlignment: CrossAxisAlignment.start,
  235. children: [
  236. Text(
  237. title,
  238. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: foreground),
  239. ),
  240. const SizedBox(height: 4),
  241. Text(
  242. subtitle,
  243. style: TextStyle(fontSize: 12, color: foreground.withOpacity(0.85)),
  244. ),
  245. ],
  246. ),
  247. ],
  248. ),
  249. ),
  250. );
  251. }
  252. }