|
|
@@ -1,11 +1,14 @@
|
|
|
import 'package:flutter/material.dart';
|
|
|
import '../services/database_helper.dart';
|
|
|
+import '../services/app_strings.dart';
|
|
|
import '../theme/theme_controller.dart';
|
|
|
+import '../theme/locale_controller.dart';
|
|
|
import '../widgets/brand_mark.dart';
|
|
|
import 'analysis_screen.dart';
|
|
|
import 'live_analysis_screen.dart';
|
|
|
import 'static_capture_screen.dart';
|
|
|
import 'history_screen.dart';
|
|
|
+import 'manual_screen.dart';
|
|
|
|
|
|
class HomeScreen extends StatefulWidget {
|
|
|
const HomeScreen({super.key});
|
|
|
@@ -32,108 +35,194 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
Widget build(BuildContext context) {
|
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
|
|
- return Scaffold(
|
|
|
- body: SafeArea(
|
|
|
- child: SingleChildScrollView(
|
|
|
- padding: const EdgeInsets.all(20),
|
|
|
- child: Column(
|
|
|
- crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
- children: [
|
|
|
- Row(
|
|
|
+ return ValueListenableBuilder<String>(
|
|
|
+ valueListenable: LocaleController.localeCode,
|
|
|
+ builder: (context, _, _) {
|
|
|
+ return Scaffold(
|
|
|
+ body: SafeArea(
|
|
|
+ child: SingleChildScrollView(
|
|
|
+ padding: const EdgeInsets.all(20),
|
|
|
+ child: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
- const BrandMark(size: 44),
|
|
|
- const SizedBox(width: 12),
|
|
|
- Expanded(
|
|
|
- child: Text(
|
|
|
- 'Palm Oil Ripeness AI',
|
|
|
- style: TextStyle(
|
|
|
- fontSize: 20,
|
|
|
- fontWeight: FontWeight.bold,
|
|
|
- color: colorScheme.onSurface,
|
|
|
+ Row(
|
|
|
+ children: [
|
|
|
+ const BrandMark(size: 44),
|
|
|
+ const SizedBox(width: 12),
|
|
|
+ Expanded(
|
|
|
+ child: Text(
|
|
|
+ AppStrings.t('app_title'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: 20,
|
|
|
+ fontWeight: FontWeight.bold,
|
|
|
+ color: colorScheme.onSurface,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
),
|
|
|
- ),
|
|
|
+ IconButton(
|
|
|
+ icon: Icon(Icons.help_outline, color: colorScheme.onSurface),
|
|
|
+ tooltip: AppStrings.t('manual_title'),
|
|
|
+ onPressed: () => Navigator.push(
|
|
|
+ context,
|
|
|
+ MaterialPageRoute(builder: (context) => const ManualScreen()),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ IconButton(
|
|
|
+ icon: Icon(Icons.settings_outlined, color: colorScheme.onSurface),
|
|
|
+ tooltip: AppStrings.t('settings_label'),
|
|
|
+ onPressed: () => _showSettingsSheet(context),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
),
|
|
|
- ValueListenableBuilder<ThemeMode>(
|
|
|
- valueListenable: ThemeController.themeMode,
|
|
|
- builder: (context, mode, _) {
|
|
|
- return IconButton(
|
|
|
- icon: Icon(_themeModeIcon(mode), color: colorScheme.onSurface),
|
|
|
- tooltip: 'Theme',
|
|
|
- onPressed: () => _showThemePicker(context, mode),
|
|
|
- );
|
|
|
- },
|
|
|
+ const SizedBox(height: 20),
|
|
|
+ _buildStatCard(colorScheme),
|
|
|
+ const SizedBox(height: 24),
|
|
|
+ GridView.count(
|
|
|
+ shrinkWrap: true,
|
|
|
+ physics: const NeverScrollableScrollPhysics(),
|
|
|
+ crossAxisCount: 2,
|
|
|
+ mainAxisSpacing: 16,
|
|
|
+ crossAxisSpacing: 16,
|
|
|
+ childAspectRatio: 0.95,
|
|
|
+ children: [
|
|
|
+ _buildTile(
|
|
|
+ context,
|
|
|
+ title: AppStrings.t('analyze_gallery_title'),
|
|
|
+ subtitle: AppStrings.t('analyze_gallery_subtitle'),
|
|
|
+ icon: Icons.photo_library,
|
|
|
+ background: colorScheme.primary,
|
|
|
+ foreground: colorScheme.onPrimary,
|
|
|
+ onTap: () => Navigator.push(
|
|
|
+ context,
|
|
|
+ MaterialPageRoute(builder: (context) => const AnalysisScreen()),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ _buildTile(
|
|
|
+ context,
|
|
|
+ title: AppStrings.t('snap_analyze_title'),
|
|
|
+ subtitle: AppStrings.t('snap_analyze_subtitle'),
|
|
|
+ icon: Icons.camera_alt,
|
|
|
+ background: colorScheme.secondary,
|
|
|
+ foreground: colorScheme.onSecondary,
|
|
|
+ onTap: () => Navigator.push(
|
|
|
+ context,
|
|
|
+ MaterialPageRoute(builder: (context) => const StaticCaptureScreen()),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ _buildTile(
|
|
|
+ context,
|
|
|
+ title: AppStrings.t('live_inference_title'),
|
|
|
+ subtitle: AppStrings.t('live_inference_subtitle'),
|
|
|
+ icon: Icons.camera,
|
|
|
+ background: colorScheme.primaryContainer,
|
|
|
+ foreground: colorScheme.onPrimaryContainer,
|
|
|
+ onTap: () => Navigator.push(
|
|
|
+ context,
|
|
|
+ MaterialPageRoute(builder: (context) => const LiveAnalysisScreen()),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ _buildTile(
|
|
|
+ context,
|
|
|
+ title: AppStrings.t('history_vault_title'),
|
|
|
+ subtitle: AppStrings.t('history_vault_subtitle'),
|
|
|
+ icon: Icons.history,
|
|
|
+ background: colorScheme.surfaceContainerHighest,
|
|
|
+ foreground: colorScheme.onSurface,
|
|
|
+ bordered: true,
|
|
|
+ onTap: () => Navigator.push(
|
|
|
+ context,
|
|
|
+ MaterialPageRoute(builder: (context) => const HistoryScreen()),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
- const SizedBox(height: 20),
|
|
|
- _buildStatCard(colorScheme),
|
|
|
- const SizedBox(height: 24),
|
|
|
- GridView.count(
|
|
|
- shrinkWrap: true,
|
|
|
- physics: const NeverScrollableScrollPhysics(),
|
|
|
- crossAxisCount: 2,
|
|
|
- mainAxisSpacing: 16,
|
|
|
- crossAxisSpacing: 16,
|
|
|
- childAspectRatio: 0.95,
|
|
|
- children: [
|
|
|
- _buildTile(
|
|
|
- context,
|
|
|
- title: 'Analyze Gallery',
|
|
|
- subtitle: 'Detect ripeness from photos',
|
|
|
- icon: Icons.photo_library,
|
|
|
- background: colorScheme.primary,
|
|
|
- foreground: colorScheme.onPrimary,
|
|
|
- onTap: () => Navigator.push(
|
|
|
- context,
|
|
|
- MaterialPageRoute(builder: (context) => const AnalysisScreen()),
|
|
|
- ),
|
|
|
- ),
|
|
|
- _buildTile(
|
|
|
- context,
|
|
|
- title: 'Snap & Analyze',
|
|
|
- subtitle: 'Manual high-res capture',
|
|
|
- icon: Icons.camera_alt,
|
|
|
- background: colorScheme.secondary,
|
|
|
- foreground: colorScheme.onSecondary,
|
|
|
- onTap: () => Navigator.push(
|
|
|
- context,
|
|
|
- MaterialPageRoute(builder: (context) => const StaticCaptureScreen()),
|
|
|
- ),
|
|
|
- ),
|
|
|
- _buildTile(
|
|
|
- context,
|
|
|
- title: 'Live Inference',
|
|
|
- subtitle: 'Real-time "Point-and-Scan"',
|
|
|
- icon: Icons.camera,
|
|
|
- background: colorScheme.primaryContainer,
|
|
|
- foreground: colorScheme.onPrimaryContainer,
|
|
|
- onTap: () => Navigator.push(
|
|
|
- context,
|
|
|
- MaterialPageRoute(builder: (context) => const LiveAnalysisScreen()),
|
|
|
- ),
|
|
|
- ),
|
|
|
- _buildTile(
|
|
|
- context,
|
|
|
- title: 'History Vault',
|
|
|
- subtitle: 'View previous detections',
|
|
|
- icon: Icons.history,
|
|
|
- background: colorScheme.surfaceContainerHighest,
|
|
|
- foreground: colorScheme.onSurface,
|
|
|
- bordered: true,
|
|
|
- onTap: () => Navigator.push(
|
|
|
- context,
|
|
|
- MaterialPageRoute(builder: (context) => const HistoryScreen()),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> _showSettingsSheet(BuildContext context) async {
|
|
|
+ await showModalBottomSheet(
|
|
|
+ context: context,
|
|
|
+ shape: const RoundedRectangleBorder(
|
|
|
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
+ ),
|
|
|
+ builder: (sheetContext) {
|
|
|
+ return SafeArea(
|
|
|
+ child: Column(
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ children: [
|
|
|
+ Padding(
|
|
|
+ padding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
|
|
|
+ child: Align(
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: Text(AppStrings.t('settings_label'), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ValueListenableBuilder<ThemeMode>(
|
|
|
+ valueListenable: ThemeController.themeMode,
|
|
|
+ builder: (context, mode, _) {
|
|
|
+ return ListTile(
|
|
|
+ leading: Icon(_themeModeIcon(mode)),
|
|
|
+ title: Text(AppStrings.t('theme_label')),
|
|
|
+ subtitle: Text(_themeModeLabel(mode)),
|
|
|
+ trailing: const Icon(Icons.chevron_right),
|
|
|
+ onTap: () {
|
|
|
+ Navigator.pop(sheetContext);
|
|
|
+ _showThemePicker(context, mode);
|
|
|
+ },
|
|
|
+ );
|
|
|
+ },
|
|
|
),
|
|
|
+ ValueListenableBuilder<String>(
|
|
|
+ valueListenable: LocaleController.localeCode,
|
|
|
+ builder: (context, code, _) {
|
|
|
+ return ListTile(
|
|
|
+ leading: const Icon(Icons.language),
|
|
|
+ title: Text(AppStrings.t('language_label')),
|
|
|
+ subtitle: Text(_languageLabel(code)),
|
|
|
+ trailing: const Icon(Icons.chevron_right),
|
|
|
+ onTap: () {
|
|
|
+ Navigator.pop(sheetContext);
|
|
|
+ _showLanguagePicker(context);
|
|
|
+ },
|
|
|
+ );
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 8),
|
|
|
],
|
|
|
),
|
|
|
- ),
|
|
|
- ),
|
|
|
+ );
|
|
|
+ },
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ String _themeModeLabel(ThemeMode mode) {
|
|
|
+ switch (mode) {
|
|
|
+ case ThemeMode.light:
|
|
|
+ return AppStrings.t('theme_light');
|
|
|
+ case ThemeMode.dark:
|
|
|
+ return AppStrings.t('theme_dark');
|
|
|
+ case ThemeMode.system:
|
|
|
+ return AppStrings.t('theme_system');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ String _languageLabel(String code) {
|
|
|
+ switch (code) {
|
|
|
+ case 'ms':
|
|
|
+ return "Bahasa Melayu";
|
|
|
+ case 'zh':
|
|
|
+ return "中文";
|
|
|
+ default:
|
|
|
+ return "English";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
IconData _themeModeIcon(ThemeMode mode) {
|
|
|
switch (mode) {
|
|
|
case ThemeMode.light:
|
|
|
@@ -156,16 +245,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
child: Column(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [
|
|
|
- const Padding(
|
|
|
- padding: EdgeInsets.fromLTRB(20, 20, 20, 8),
|
|
|
+ Padding(
|
|
|
+ padding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
|
|
|
child: Align(
|
|
|
alignment: Alignment.centerLeft,
|
|
|
- child: Text("Theme", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
|
+ child: Text(AppStrings.t('theme_label'), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
|
),
|
|
|
),
|
|
|
- _themeOptionTile(sheetContext, "Light", Icons.light_mode, ThemeMode.light, current),
|
|
|
- _themeOptionTile(sheetContext, "Dark", Icons.dark_mode, ThemeMode.dark, current),
|
|
|
- _themeOptionTile(sheetContext, "System", Icons.brightness_auto, ThemeMode.system, current),
|
|
|
+ _themeOptionTile(sheetContext, AppStrings.t('theme_light'), Icons.light_mode, ThemeMode.light, current),
|
|
|
+ _themeOptionTile(sheetContext, AppStrings.t('theme_dark'), Icons.dark_mode, ThemeMode.dark, current),
|
|
|
+ _themeOptionTile(sheetContext, AppStrings.t('theme_system'), Icons.brightness_auto, ThemeMode.system, current),
|
|
|
const SizedBox(height: 8),
|
|
|
],
|
|
|
),
|
|
|
@@ -187,6 +276,48 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ Future<void> _showLanguagePicker(BuildContext context) async {
|
|
|
+ final current = LocaleController.localeCode.value;
|
|
|
+ await showModalBottomSheet(
|
|
|
+ context: context,
|
|
|
+ shape: const RoundedRectangleBorder(
|
|
|
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
+ ),
|
|
|
+ builder: (sheetContext) {
|
|
|
+ return SafeArea(
|
|
|
+ child: Column(
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ children: [
|
|
|
+ Padding(
|
|
|
+ padding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
|
|
|
+ child: Align(
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: Text(AppStrings.t('language_label'), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ _languageOptionTile(sheetContext, "English", "en", current),
|
|
|
+ _languageOptionTile(sheetContext, "Bahasa Melayu", "ms", current),
|
|
|
+ _languageOptionTile(sheetContext, "中文", "zh", current),
|
|
|
+ const SizedBox(height: 8),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget _languageOptionTile(BuildContext context, String label, String code, String current) {
|
|
|
+ final selected = code == current;
|
|
|
+ return ListTile(
|
|
|
+ title: Text(label),
|
|
|
+ trailing: selected ? const Icon(Icons.check) : null,
|
|
|
+ onTap: () {
|
|
|
+ LocaleController.setLocale(code);
|
|
|
+ Navigator.pop(context);
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
Widget _buildStatCard(ColorScheme colorScheme) {
|
|
|
return Container(
|
|
|
width: double.infinity,
|
|
|
@@ -194,7 +325,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
decoration: BoxDecoration(
|
|
|
color: colorScheme.surfaceContainerHighest,
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
- border: Border.all(color: colorScheme.primary.withOpacity(0.15)),
|
|
|
+ border: Border.all(color: colorScheme.primary.withValues(alpha: 0.15)),
|
|
|
),
|
|
|
child: Row(
|
|
|
children: [
|
|
|
@@ -208,8 +339,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: colorScheme.onSurface),
|
|
|
),
|
|
|
Text(
|
|
|
- 'Total Scans',
|
|
|
- style: TextStyle(fontSize: 13, color: colorScheme.onSurface.withOpacity(0.6)),
|
|
|
+ AppStrings.t('total_scans'),
|
|
|
+ style: TextStyle(fontSize: 13, color: colorScheme.onSurface.withValues(alpha: 0.6)),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
@@ -235,7 +366,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
decoration: BoxDecoration(
|
|
|
color: background,
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
- border: bordered ? Border.all(color: foreground.withOpacity(0.15)) : null,
|
|
|
+ border: bordered ? Border.all(color: foreground.withValues(alpha: 0.15)) : null,
|
|
|
),
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
@@ -252,7 +383,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
const SizedBox(height: 4),
|
|
|
Text(
|
|
|
subtitle,
|
|
|
- style: TextStyle(fontSize: 12, color: foreground.withOpacity(0.85)),
|
|
|
+ style: TextStyle(fontSize: 12, color: foreground.withValues(alpha: 0.85)),
|
|
|
),
|
|
|
],
|
|
|
),
|