1010 lines
31 KiB
Dart
1010 lines
31 KiB
Dart
import 'dart:io';
|
|
import 'dart:math' as math;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'camera_screen.dart';
|
|
import '../../services/firebase_service.dart';
|
|
import '../../models/recommendation_model.dart';
|
|
import '../recommendation/recommendation_screen.dart';
|
|
|
|
/// Layar pemilihan foto menu — UX seperti upload gambar di AI.
|
|
/// User bisa tambah/hapus foto (maks 3) sebelum scan.
|
|
class PhotoPickerScreen extends StatefulWidget {
|
|
final List<String> initialPaths;
|
|
const PhotoPickerScreen({super.key, this.initialPaths = const []});
|
|
|
|
@override
|
|
State<PhotoPickerScreen> createState() => _PhotoPickerScreenState();
|
|
}
|
|
|
|
class _PhotoPickerScreenState extends State<PhotoPickerScreen> {
|
|
final List<String> _selectedPaths = [];
|
|
bool _isLoading = false;
|
|
List<RecommendationModel> _history = [];
|
|
bool _isLoadingHistory = true;
|
|
|
|
static const int _maxPhotos = 3;
|
|
static const Color _brown = Color(0xFF8B5E3C);
|
|
static const Color _darkBrown = Color(0xFF3C2415);
|
|
static const Color _bgColor = Color(0xFFFAF7F4);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedPaths.addAll(widget.initialPaths);
|
|
_loadHistory();
|
|
}
|
|
|
|
Future<void> _loadHistory() async {
|
|
try {
|
|
final user = FirebaseService().currentUser;
|
|
if (user != null) {
|
|
final allHistory = await FirebaseService().getUserRecommendations(user.uid, limit: 30);
|
|
final Map<String, RecommendationModel> uniqueMenus = {};
|
|
final now = DateTime.now();
|
|
|
|
for (var rec in allHistory) {
|
|
// Otomatis hapus draf yang lebih dari 24 jam dan belum dipilih
|
|
if (now.difference(rec.createdAt).inHours > 24) {
|
|
if (rec.selectedMenuName == null) {
|
|
FirebaseService().deleteRecommendation(rec.id);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
final key = rec.originalMenuText.length > 50
|
|
? rec.originalMenuText.substring(0, 50)
|
|
: rec.originalMenuText;
|
|
if (!uniqueMenus.containsKey(key)) {
|
|
uniqueMenus[key] = rec;
|
|
}
|
|
}
|
|
if (mounted) {
|
|
setState(() {
|
|
_history = uniqueMenus.values.take(3).toList();
|
|
_isLoadingHistory = false;
|
|
});
|
|
}
|
|
} else {
|
|
if (mounted) setState(() => _isLoadingHistory = false);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) setState(() => _isLoadingHistory = false);
|
|
}
|
|
}
|
|
|
|
// ── Actions ──────────────────────────────────────────────────────────────
|
|
|
|
Future<void> _pickFromGallery() async {
|
|
if (_selectedPaths.length >= _maxPhotos) {
|
|
_showMaxReached();
|
|
return;
|
|
}
|
|
try {
|
|
final remaining = _maxPhotos - _selectedPaths.length;
|
|
final images = await ImagePicker().pickMultiImage(
|
|
imageQuality: 80,
|
|
maxWidth: 1920,
|
|
maxHeight: 1920,
|
|
limit: remaining,
|
|
);
|
|
if (images.isEmpty) return;
|
|
setState(
|
|
() => _selectedPaths.addAll(images.take(remaining).map((f) => f.path)),
|
|
);
|
|
} catch (_) {
|
|
_showError('Gagal membuka galeri');
|
|
}
|
|
}
|
|
|
|
Future<void> _pickFromCamera() async {
|
|
if (_selectedPaths.length >= _maxPhotos) {
|
|
_showMaxReached();
|
|
return;
|
|
}
|
|
try {
|
|
final image = await ImagePicker().pickImage(
|
|
source: ImageSource.camera,
|
|
imageQuality: 80,
|
|
maxWidth: 1920,
|
|
maxHeight: 1920,
|
|
);
|
|
if (image == null) return;
|
|
if (await File(image.path).exists())
|
|
setState(() => _selectedPaths.add(image.path));
|
|
} catch (_) {
|
|
_showError('Gagal membuka kamera');
|
|
}
|
|
}
|
|
|
|
void _removePhoto(int index) =>
|
|
setState(() => _selectedPaths.removeAt(index));
|
|
|
|
void _showMaxReached() {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: const Text(
|
|
'Maksimal 3 foto dapat dipilih',
|
|
style: TextStyle(fontFamily: 'Poppins'),
|
|
),
|
|
backgroundColor: _brown,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
duration: const Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showError(String msg) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(msg, style: const TextStyle(fontFamily: 'Poppins')),
|
|
backgroundColor: Colors.red.shade600,
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _deleteHistory(RecommendationModel rec) async {
|
|
final confirm = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('Hapus Draf?', style: TextStyle(fontFamily: 'Poppins', fontWeight: FontWeight.bold, color: _darkBrown)),
|
|
content: const Text('Draf menu ini akan dihapus dari riwayat scan Anda.'),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
child: const Text('Batal', style: TextStyle(color: Colors.grey)),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(ctx, true),
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red.shade600),
|
|
child: const Text('Hapus', style: TextStyle(color: Colors.white)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirm == true) {
|
|
setState(() {
|
|
_history.remove(rec);
|
|
});
|
|
// Hanya hapus dari database jika belum pernah dipilih ke riwayat utama
|
|
if (rec.selectedMenuName == null) {
|
|
FirebaseService().deleteRecommendation(rec.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _proceedToScan() async {
|
|
if (_selectedPaths.isEmpty) return;
|
|
setState(() => _isLoading = true);
|
|
await Future.delayed(const Duration(milliseconds: 80));
|
|
if (!mounted) return;
|
|
await Navigator.of(context).push(
|
|
PageRouteBuilder(
|
|
pageBuilder: (_, a, __) =>
|
|
CameraScreen(imagePaths: List.from(_selectedPaths)),
|
|
transitionsBuilder: (_, a, __, child) => SlideTransition(
|
|
position: Tween(
|
|
begin: const Offset(0, 1),
|
|
end: Offset.zero,
|
|
).chain(CurveTween(curve: Curves.easeOutCubic)).animate(a),
|
|
child: child,
|
|
),
|
|
transitionDuration: const Duration(milliseconds: 350),
|
|
),
|
|
);
|
|
if (mounted) setState(() => _isLoading = false);
|
|
}
|
|
|
|
Future<void> _proceedWithHistory(RecommendationModel scan) async {
|
|
setState(() => _isLoading = true);
|
|
await Future.delayed(const Duration(milliseconds: 80));
|
|
if (!mounted) return;
|
|
await Navigator.of(context).push(
|
|
PageRouteBuilder(
|
|
pageBuilder: (_, a, __) =>
|
|
RecommendationScreen(recommendation: scan, ocrResult: null),
|
|
transitionsBuilder: (_, a, __, child) => SlideTransition(
|
|
position: Tween(
|
|
begin: const Offset(0, 1),
|
|
end: Offset.zero,
|
|
).chain(CurveTween(curve: Curves.easeOutCubic)).animate(a),
|
|
child: child,
|
|
),
|
|
transitionDuration: const Duration(milliseconds: 350),
|
|
),
|
|
);
|
|
if (mounted) setState(() => _isLoading = false);
|
|
}
|
|
|
|
void _showAddPhotoMenu() {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (ctx) => Container(
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
padding: const EdgeInsets.fromLTRB(24, 16, 24, 32),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFD4CFC8),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
const Text(
|
|
'Tambah Foto Menu',
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 17,
|
|
color: _darkBrown,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'${_selectedPaths.length}/$_maxPhotos foto dipilih',
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontSize: 12,
|
|
color: _brown.withOpacity(0.65),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildSheetOption(
|
|
icon: Icons.photo_library_rounded,
|
|
label: 'Galeri',
|
|
subtitle: 'Pilih beberapa',
|
|
color: _brown,
|
|
onTap: () {
|
|
Navigator.pop(ctx);
|
|
_pickFromGallery();
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildSheetOption(
|
|
icon: Icons.camera_alt_rounded,
|
|
label: 'Kamera',
|
|
subtitle: 'Ambil foto',
|
|
color: const Color(0xFF6B3A2A),
|
|
onTap: () {
|
|
Navigator.pop(ctx);
|
|
_pickFromCamera();
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Build ─────────────────────────────────────────────────────────────────
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: _bgColor,
|
|
appBar: _buildAppBar(),
|
|
body: SafeArea(
|
|
child: Column(
|
|
children: [
|
|
Expanded(child: _buildBody()),
|
|
_buildBottomBar(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
PreferredSizeWidget _buildAppBar() => AppBar(
|
|
backgroundColor: Colors.white,
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const Icon(
|
|
Icons.arrow_back_ios_rounded,
|
|
color: _darkBrown,
|
|
size: 20,
|
|
),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
title: const Text(
|
|
'Pilih Foto Menu',
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 18,
|
|
color: _darkBrown,
|
|
),
|
|
),
|
|
centerTitle: true,
|
|
bottom: PreferredSize(
|
|
preferredSize: const Size.fromHeight(1),
|
|
child: Container(height: 1, color: const Color(0xFFF0EBE4)),
|
|
),
|
|
);
|
|
|
|
Widget _buildBody() => SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildInfoHeader(),
|
|
const SizedBox(height: 20),
|
|
_buildPhotoGrid(),
|
|
const SizedBox(height: 24),
|
|
_buildHistorySection(),
|
|
const SizedBox(height: 24),
|
|
_buildTips(),
|
|
],
|
|
),
|
|
);
|
|
|
|
Widget _buildInfoHeader() {
|
|
final count = _selectedPaths.length;
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [_brown.withOpacity(0.08), _brown.withOpacity(0.03)],
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: _brown.withOpacity(0.15)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: _brown.withOpacity(0.12),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const Icon(
|
|
Icons.document_scanner_rounded,
|
|
color: _brown,
|
|
size: 22,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
count == 0 ? 'Belum ada foto dipilih' : '$count foto dipilih',
|
|
style: const TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 14,
|
|
color: _darkBrown,
|
|
),
|
|
),
|
|
Text(
|
|
'Maksimal $_maxPhotos foto menu · ketuk slot untuk menambah',
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontSize: 11,
|
|
color: _brown.withOpacity(0.7),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
_buildDotProgress(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDotProgress() => Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: List.generate(_maxPhotos, (i) {
|
|
final filled = i < _selectedPaths.length;
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
margin: const EdgeInsets.only(left: 5),
|
|
width: filled ? 10 : 8,
|
|
height: filled ? 10 : 8,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: filled ? _brown : _brown.withOpacity(0.2),
|
|
border: Border.all(
|
|
color: filled ? _brown : _brown.withOpacity(0.3),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
|
|
Widget _buildPhotoGrid() {
|
|
final count = _selectedPaths.length;
|
|
final showAdd = count < _maxPhotos;
|
|
final totalCells = count + (showAdd ? 1 : 0);
|
|
|
|
return GridView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 3,
|
|
crossAxisSpacing: 10,
|
|
mainAxisSpacing: 10,
|
|
childAspectRatio: 0.85,
|
|
),
|
|
itemCount: totalCells,
|
|
itemBuilder: (ctx, i) =>
|
|
i < count ? _buildPhotoThumbnail(i) : _buildAddSlot(),
|
|
);
|
|
}
|
|
|
|
Widget _buildPhotoThumbnail(int index) => Stack(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(14),
|
|
child: Image.file(
|
|
File(_selectedPaths[index]),
|
|
fit: BoxFit.cover,
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
errorBuilder: (_, __, ___) => Container(
|
|
color: const Color(0xFFE8E0D8),
|
|
child: const Icon(
|
|
Icons.broken_image_rounded,
|
|
color: Colors.white54,
|
|
size: 32,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Gradient overlay atas
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: Container(
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(14)),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.black.withOpacity(0.35), Colors.transparent],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Nomor badge
|
|
Positioned(
|
|
top: 6,
|
|
left: 8,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: _brown,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
'${index + 1}',
|
|
style: const TextStyle(
|
|
fontFamily: 'Poppins',
|
|
color: Colors.white,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Tombol hapus
|
|
Positioned(
|
|
top: 4,
|
|
right: 4,
|
|
child: GestureDetector(
|
|
onTap: () => _removePhoto(index),
|
|
child: Container(
|
|
width: 26,
|
|
height: 26,
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.shade600,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.25),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.close_rounded,
|
|
color: Colors.white,
|
|
size: 14,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
Widget _buildHistorySection() {
|
|
if (_isLoadingHistory) {
|
|
return const Center(child: CircularProgressIndicator(color: _brown));
|
|
}
|
|
if (_history.isEmpty) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Gunakan Menu Sebelumnya',
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 16,
|
|
color: _darkBrown,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
height: 140,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: _history.length,
|
|
separatorBuilder: (context, index) => const SizedBox(width: 12),
|
|
itemBuilder: (context, index) {
|
|
final rec = _history[index];
|
|
// Buat judul simulasi dari menu rekomendasi pertama
|
|
final title = rec.recommendations.isNotEmpty
|
|
? rec.recommendations.first.menuName
|
|
: 'Menu Cafe';
|
|
|
|
return Stack(
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () => _proceedWithHistory(rec),
|
|
child: Container(
|
|
width: 130,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: _brown.withOpacity(0.15)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: _brown.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
|
child: _buildHistoryImage(rec),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 12,
|
|
color: _darkBrown,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'${rec.recommendations.length} menu',
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontSize: 10,
|
|
color: _brown.withOpacity(0.8),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
top: 6,
|
|
right: 6,
|
|
child: GestureDetector(
|
|
onTap: () => _deleteHistory(rec),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.shade600,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.2),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.close_rounded,
|
|
size: 14,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildHistoryImage(RecommendationModel rec) {
|
|
if (rec.menuImageUrl != null && rec.menuImageUrl!.startsWith('http')) {
|
|
return Image.network(
|
|
rec.menuImageUrl!,
|
|
height: 70,
|
|
width: double.infinity,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (_, __, ___) => _fallbackImage(),
|
|
);
|
|
} else if (rec.menuImagePaths != null && rec.menuImagePaths!.isNotEmpty) {
|
|
final file = File(rec.menuImagePaths!.first);
|
|
if (file.existsSync()) {
|
|
return Image.file(
|
|
file,
|
|
height: 70,
|
|
width: double.infinity,
|
|
fit: BoxFit.cover,
|
|
);
|
|
}
|
|
}
|
|
return _fallbackImage();
|
|
}
|
|
|
|
Widget _fallbackImage() => Container(
|
|
height: 70,
|
|
width: double.infinity,
|
|
color: _brown.withOpacity(0.1),
|
|
child: Icon(Icons.restaurant_menu_rounded, color: _brown.withOpacity(0.5)),
|
|
);
|
|
|
|
/// Slot kosong bertitik dashed — mengindikasikan "bisa tambah lebih" tanpa tombol eksplisit
|
|
Widget _buildAddSlot() => GestureDetector(
|
|
onTap: _showAddPhotoMenu,
|
|
child: CustomPaint(
|
|
painter: _DashedBorderPainter(color: _brown.withOpacity(0.3), radius: 14),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: _brown.withOpacity(0.025),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Dua ikon mini berdampingan sebagai hint sumber
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
_miniIcon(Icons.photo_library_rounded),
|
|
Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 6),
|
|
width: 1,
|
|
height: 16,
|
|
color: _brown.withOpacity(0.2),
|
|
),
|
|
_miniIcon(Icons.camera_alt_rounded),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Lingkaran "+" kecil
|
|
Container(
|
|
width: 20,
|
|
height: 20,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: _brown.withOpacity(0.45), width: 1.5),
|
|
),
|
|
child: Icon(
|
|
Icons.add_rounded,
|
|
size: 13,
|
|
color: _brown.withOpacity(0.6),
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'${_maxPhotos - _selectedPaths.length} slot tersisa',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w500,
|
|
color: _brown.withOpacity(0.5),
|
|
height: 1.3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
Widget _miniIcon(IconData icon) => Container(
|
|
padding: const EdgeInsets.all(5),
|
|
decoration: BoxDecoration(
|
|
color: _brown.withOpacity(0.08),
|
|
borderRadius: BorderRadius.circular(7),
|
|
),
|
|
child: Icon(icon, size: 14, color: _brown.withOpacity(0.6)),
|
|
);
|
|
|
|
Widget _buildTips() => Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: const Color(0xFFE8DFD4)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(
|
|
Icons.lightbulb_outline_rounded,
|
|
size: 15,
|
|
color: Color(0xFFE5A100),
|
|
),
|
|
SizedBox(width: 6),
|
|
Text(
|
|
'Tips Scan Menu',
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 12,
|
|
color: _darkBrown,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
...[
|
|
'Foto menu dengan pencahayaan yang baik',
|
|
'Jika menu terdiri dari beberapa halaman, tambah semua',
|
|
'Pastikan tulisan harga terbaca jelas',
|
|
].map(
|
|
(tip) => Padding(
|
|
padding: const EdgeInsets.only(top: 5),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'• ',
|
|
style: TextStyle(
|
|
color: _brown,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
tip,
|
|
style: const TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontSize: 11,
|
|
color: Color(0xFF6B5B4E),
|
|
height: 1.4,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
Widget _buildBottomBar() => Container(
|
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: const Border(top: BorderSide(color: Color(0xFFF0EBE4))),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, -4),
|
|
),
|
|
],
|
|
),
|
|
child: SafeArea(
|
|
top: false,
|
|
child: AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 300),
|
|
child: _selectedPaths.isNotEmpty
|
|
? _buildScanButton()
|
|
: _buildEmptyPrompt(),
|
|
),
|
|
),
|
|
);
|
|
|
|
Widget _buildScanButton() => SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: _isLoading ? null : _proceedToScan,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _brown,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
elevation: 4,
|
|
shadowColor: _brown.withOpacity(0.4),
|
|
),
|
|
child: _isLoading
|
|
? const SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.document_scanner_rounded, size: 20),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
'Scan ${_selectedPaths.length} Foto Menu',
|
|
style: const TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
Widget _buildEmptyPrompt() => Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.touch_app_rounded, size: 16, color: _brown.withOpacity(0.5)),
|
|
const SizedBox(width: 8),
|
|
Flexible(
|
|
child: Text(
|
|
'Ketuk slot bergaris putus untuk menambah foto',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontSize: 12,
|
|
color: _brown.withOpacity(0.6),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
Widget _buildSheetOption({
|
|
required IconData icon,
|
|
required String label,
|
|
required String subtitle,
|
|
required Color color,
|
|
required VoidCallback onTap,
|
|
}) => GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 18),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [color, color.withOpacity(0.8)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, color: Colors.white, size: 28),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontFamily: 'Poppins',
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
color: Colors.white.withOpacity(0.8),
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// CustomPainter untuk border dashed pada slot tambah foto
|
|
class _DashedBorderPainter extends CustomPainter {
|
|
final Color color;
|
|
final double radius;
|
|
final double dashWidth;
|
|
final double dashGap;
|
|
|
|
const _DashedBorderPainter({
|
|
required this.color,
|
|
this.radius = 12,
|
|
this.dashWidth = 6,
|
|
this.dashGap = 4,
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()
|
|
..color = color
|
|
..strokeWidth = 1.5
|
|
..style = PaintingStyle.stroke;
|
|
|
|
final path = Path()
|
|
..addRRect(
|
|
RRect.fromRectAndRadius(
|
|
Rect.fromLTWH(1, 1, size.width - 2, size.height - 2),
|
|
Radius.circular(radius),
|
|
),
|
|
);
|
|
|
|
final metrics = path.computeMetrics();
|
|
for (final metric in metrics) {
|
|
double distance = 0;
|
|
while (distance < metric.length) {
|
|
final len = math.min(dashWidth, metric.length - distance);
|
|
canvas.drawPath(metric.extractPath(distance, distance + len), paint);
|
|
distance += dashWidth + dashGap;
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(_DashedBorderPainter old) =>
|
|
old.color != color || old.radius != radius;
|
|
}
|