941 lines
41 KiB
Dart
941 lines
41 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:tugas_akhir_supabase/core/theme/app_colors.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/models/farming_guide_model.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/utils/plant_categorizer.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/services/guide_service.dart';
|
|
import 'dart:io';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
class GuideManagement extends StatefulWidget {
|
|
const GuideManagement({super.key});
|
|
|
|
@override
|
|
State<GuideManagement> createState() => _GuideManagementState();
|
|
}
|
|
|
|
class _GuideManagementState extends State<GuideManagement> {
|
|
final _supabase = Supabase.instance.client;
|
|
final _guideService = GuideService();
|
|
bool _isLoading = true;
|
|
List<FarmingGuideModel> _guides = [];
|
|
String _searchQuery = '';
|
|
|
|
// Detected storage bucket
|
|
String _storageBucket = 'images';
|
|
|
|
// Controller untuk form tambah/edit
|
|
final _titleController = TextEditingController();
|
|
final _contentController = TextEditingController();
|
|
final _categoryController = TextEditingController();
|
|
String? _selectedImagePath;
|
|
String? _currentGuideId;
|
|
bool _isFormLoading = false;
|
|
|
|
// Modern Color Scheme
|
|
static const Color primaryGreen = Color(0xFF0F6848);
|
|
static const Color lightGreen = Color(0xFF4CAF50);
|
|
static const Color surfaceGreen = Color(0xFFF1F8E9);
|
|
static const Color cardWhite = Colors.white;
|
|
static const Color textPrimary = Color(0xFF1B5E20);
|
|
static const Color textSecondary = Color(0xFF757575);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_detectStorageBucket();
|
|
_loadGuides();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_titleController.dispose();
|
|
_contentController.dispose();
|
|
_categoryController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _detectStorageBucket() async {
|
|
try {
|
|
// Get bucket names
|
|
final buckets = await _supabase.storage.listBuckets();
|
|
final bucketNames = buckets.map((b) => b.name).toList();
|
|
|
|
debugPrint('Available buckets: ${bucketNames.join(', ')}');
|
|
|
|
// Same logic as GuideService
|
|
if (bucketNames.contains('images')) {
|
|
_storageBucket = 'images';
|
|
} else if (bucketNames.contains('guide_images')) {
|
|
_storageBucket = 'guide_images';
|
|
} else if (bucketNames.contains('avatars')) {
|
|
_storageBucket = 'avatars';
|
|
} else if (bucketNames.isNotEmpty) {
|
|
_storageBucket = bucketNames.first;
|
|
}
|
|
|
|
debugPrint('Selected bucket for guide images: $_storageBucket');
|
|
} catch (e) {
|
|
debugPrint('Error detecting buckets: $e');
|
|
// Keep default bucket
|
|
}
|
|
}
|
|
|
|
Future<void> _loadGuides() async {
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
// Fetch all guides from the database
|
|
final response = await _supabase
|
|
.from('farming_guides')
|
|
.select('*')
|
|
.order('created_at', ascending: false);
|
|
|
|
debugPrint('Guides loaded: ${response.length}');
|
|
|
|
// Convert to List<FarmingGuideModel>
|
|
final guides =
|
|
List<Map<String, dynamic>>.from(
|
|
response,
|
|
).map((map) => FarmingGuideModel.fromMap(map)).toList();
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_guides = guides;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error loading guides: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoading = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Error: Unable to load guides. ${e.toString()}'),
|
|
backgroundColor: Colors.red.shade400,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _filterGuides(String query) {
|
|
setState(() {
|
|
_searchQuery = query;
|
|
if (query.isEmpty) {
|
|
// No need to reload, just display all guides
|
|
} else {
|
|
// Filter locally for better performance
|
|
// In a real app with many guides, you might want to do this on the server
|
|
}
|
|
});
|
|
}
|
|
|
|
List<FarmingGuideModel> get _filteredGuides {
|
|
if (_searchQuery.isEmpty) {
|
|
return _guides;
|
|
}
|
|
|
|
final query = _searchQuery.toLowerCase();
|
|
return _guides.where((guide) {
|
|
final title = guide.title.toLowerCase();
|
|
final category = guide.category.toLowerCase();
|
|
final content = guide.content.toLowerCase();
|
|
|
|
return title.contains(query) ||
|
|
category.contains(query) ||
|
|
content.contains(query);
|
|
}).toList();
|
|
}
|
|
|
|
Future<void> _pickImage() async {
|
|
try {
|
|
debugPrint('Memulai proses pemilihan gambar...');
|
|
// Pendekatan yang lebih sederhana tanpa permission handler
|
|
final ImagePicker picker = ImagePicker();
|
|
final XFile? image = await picker.pickImage(
|
|
source: ImageSource.gallery,
|
|
// Tanpa pembatasan ukuran
|
|
);
|
|
|
|
if (image != null) {
|
|
debugPrint('Gambar dipilih: ${image.path}');
|
|
if (mounted) {
|
|
setState(() {
|
|
_selectedImagePath = image.path;
|
|
});
|
|
}
|
|
} else {
|
|
debugPrint('Pemilihan gambar dibatalkan');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error saat memilih gambar: $e');
|
|
// Jangan tampilkan error ke user untuk menghindari crash lanjutan
|
|
}
|
|
}
|
|
|
|
Future<String?> _uploadImage() async {
|
|
if (_selectedImagePath == null) return null;
|
|
|
|
try {
|
|
debugPrint('Mulai upload gambar sederhana dari $_selectedImagePath');
|
|
final file = File(_selectedImagePath!);
|
|
|
|
if (!await file.exists()) {
|
|
debugPrint('File tidak ditemukan: $_selectedImagePath');
|
|
return null;
|
|
}
|
|
|
|
final fileExtension = path.extension(file.path).toLowerCase();
|
|
final fileName =
|
|
'guide_${DateTime.now().millisecondsSinceEpoch}$fileExtension';
|
|
|
|
debugPrint(
|
|
'Mencoba upload dengan nama file: $fileName ke bucket $_storageBucket',
|
|
);
|
|
|
|
// Baca file sebagai bytes untuk menghindari masalah path
|
|
final bytes = await file.readAsBytes();
|
|
|
|
// Upload sebagai binary ke bucket yang terdeteksi
|
|
await _supabase.storage
|
|
.from(_storageBucket)
|
|
.uploadBinary(
|
|
fileName,
|
|
bytes,
|
|
fileOptions: const FileOptions(contentType: 'image/jpeg'),
|
|
);
|
|
|
|
// Get public URL - Gunakan metode yang benar untuk mendapatkan URL publik
|
|
final imageUrl = _supabase.storage
|
|
.from(_storageBucket)
|
|
.getPublicUrl(fileName);
|
|
|
|
debugPrint('Gambar berhasil diupload: $imageUrl');
|
|
|
|
// Tambahkan pengecekan URL
|
|
if (!imageUrl.startsWith('http')) {
|
|
debugPrint('Warning: URL tidak dimulai dengan http: $imageUrl');
|
|
}
|
|
|
|
return imageUrl;
|
|
} catch (e) {
|
|
debugPrint('Error uploading image: $e');
|
|
|
|
// Jangan tampilkan error ke user untuk menghindari crash
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void _showAddEditGuideDialog({FarmingGuideModel? guide}) {
|
|
// Reset form or fill with guide data if editing
|
|
_currentGuideId = guide?.id;
|
|
_titleController.text = guide?.title ?? '';
|
|
_contentController.text = guide?.content ?? '';
|
|
_categoryController.text = guide?.category ?? '';
|
|
_selectedImagePath = null; // Reset selected image
|
|
|
|
// Pilihan kategori untuk dropdown
|
|
final List<String> categoryOptions = [
|
|
PlantCategorizer.TANAMAN_PANGAN,
|
|
PlantCategorizer.SAYURAN,
|
|
PlantCategorizer.BUAH_BUAHAN,
|
|
PlantCategorizer.REMPAH,
|
|
'Kalender Tanam',
|
|
PlantCategorizer.UMUM,
|
|
];
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(context) => StatefulBuilder(
|
|
builder:
|
|
(context, setDialogState) => AlertDialog(
|
|
title: Text(
|
|
guide == null ? 'Tambah Panduan Baru' : 'Edit Panduan',
|
|
),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
TextField(
|
|
controller: _titleController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Judul',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
onChanged: (value) {
|
|
// Auto-kategorisasi berdasarkan judul
|
|
if (_categoryController.text.isEmpty ||
|
|
_categoryController.text ==
|
|
PlantCategorizer.UMUM) {
|
|
final suggestedCategory =
|
|
PlantCategorizer.categorize(
|
|
value,
|
|
description: _contentController.text,
|
|
);
|
|
|
|
if (suggestedCategory != PlantCategorizer.UMUM) {
|
|
setDialogState(() {
|
|
_categoryController.text = suggestedCategory;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
// Kategori dropdown daripada text field
|
|
DropdownButtonFormField<String>(
|
|
value:
|
|
categoryOptions.contains(_categoryController.text)
|
|
? _categoryController.text
|
|
: PlantCategorizer.UMUM,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Kategori',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items:
|
|
categoryOptions.map((String category) {
|
|
return DropdownMenuItem<String>(
|
|
value: category,
|
|
child: Text(category),
|
|
);
|
|
}).toList(),
|
|
onChanged: (String? newValue) {
|
|
if (newValue != null) {
|
|
setDialogState(() {
|
|
_categoryController.text = newValue;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: _contentController,
|
|
maxLines: 8,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Konten Panduan',
|
|
border: OutlineInputBorder(),
|
|
alignLabelWithHint: true,
|
|
),
|
|
onChanged: (value) {
|
|
// Auto-kategorisasi berdasarkan konten
|
|
if (_categoryController.text.isEmpty ||
|
|
_categoryController.text ==
|
|
PlantCategorizer.UMUM) {
|
|
final suggestedCategory =
|
|
PlantCategorizer.categorize(
|
|
_titleController.text,
|
|
description: value,
|
|
);
|
|
|
|
if (suggestedCategory != PlantCategorizer.UMUM) {
|
|
setDialogState(() {
|
|
_categoryController.text = suggestedCategory;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
_selectedImagePath != null
|
|
? 'Gambar dipilih: ${path.basename(_selectedImagePath!)}'
|
|
: guide?.imageUrl != null
|
|
? 'Gambar saat ini akan dipertahankan'
|
|
: 'Belum ada gambar dipilih',
|
|
style: TextStyle(color: Colors.grey[600]),
|
|
),
|
|
),
|
|
TextButton.icon(
|
|
onPressed: () async {
|
|
await _pickImage();
|
|
setDialogState(() {}); // Update dialog state
|
|
},
|
|
icon: const Icon(Icons.image),
|
|
label: const Text('Pilih Gambar'),
|
|
),
|
|
],
|
|
),
|
|
if (_selectedImagePath != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Container(
|
|
height: 100,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[300],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.image, color: Colors.grey[700]),
|
|
const SizedBox(width: 8),
|
|
Flexible(
|
|
child: Text(
|
|
'Gambar dipilih: ${path.basename(_selectedImagePath!)}',
|
|
style: TextStyle(
|
|
color: Colors.grey[700],
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (guide?.imageUrl != null &&
|
|
_selectedImagePath == null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Container(
|
|
height: 100,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[300],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.image, color: Colors.grey[700]),
|
|
const SizedBox(width: 8),
|
|
const Flexible(
|
|
child: Text(
|
|
'Gambar sudah tersimpan',
|
|
style: TextStyle(color: Colors.grey),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (_isFormLoading)
|
|
const Padding(
|
|
padding: EdgeInsets.only(top: 16.0),
|
|
child: Center(child: CircularProgressIndicator()),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Batal'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed:
|
|
_isFormLoading
|
|
? null
|
|
: () async {
|
|
// Validate form
|
|
if (_titleController.text.trim().isEmpty ||
|
|
_contentController.text.trim().isEmpty ||
|
|
_categoryController.text.trim().isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Semua field harus diisi'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setDialogState(() => _isFormLoading = true);
|
|
|
|
try {
|
|
// Upload image if selected
|
|
String? imageUrl;
|
|
if (_selectedImagePath != null) {
|
|
imageUrl = await _uploadImage();
|
|
// Jika upload gagal, lanjutkan tanpa gambar
|
|
if (imageUrl == null) {
|
|
debugPrint(
|
|
'Upload gambar gagal, melanjutkan tanpa gambar',
|
|
);
|
|
} else {
|
|
debugPrint(
|
|
'Image URL setelah upload: $imageUrl',
|
|
);
|
|
}
|
|
}
|
|
|
|
// Persiapkan data guide
|
|
final guideData = {
|
|
'title': _titleController.text.trim(),
|
|
'content': _contentController.text.trim(),
|
|
'category': _categoryController.text.trim(),
|
|
};
|
|
|
|
// Tambahkan image_url jika ada gambar baru yang berhasil diupload
|
|
if (imageUrl != null) {
|
|
guideData['image_url'] = imageUrl;
|
|
debugPrint(
|
|
'Menambahkan image_url ke data: $imageUrl',
|
|
);
|
|
} else if (guide != null &&
|
|
guide.imageUrl != null) {
|
|
// Pertahankan image_url yang sudah ada jika tidak ada gambar baru
|
|
guideData['image_url'] = guide.imageUrl!;
|
|
debugPrint(
|
|
'Mempertahankan image_url yang ada: ${guide.imageUrl}',
|
|
);
|
|
}
|
|
|
|
if (_currentGuideId != null) {
|
|
// Update existing guide
|
|
await _supabase
|
|
.from('farming_guides')
|
|
.update(guideData)
|
|
.eq('id', _currentGuideId!);
|
|
debugPrint(
|
|
'Berhasil update guide dengan ID: $_currentGuideId',
|
|
);
|
|
} else {
|
|
// Add new guide
|
|
final response = await _supabase
|
|
.from('farming_guides')
|
|
.insert(guideData)
|
|
.select('id');
|
|
|
|
if (response.isNotEmpty) {
|
|
debugPrint(
|
|
'Berhasil insert guide dengan ID: ${response[0]['id']}',
|
|
);
|
|
}
|
|
}
|
|
|
|
// Reload guides
|
|
await _loadGuides();
|
|
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
_currentGuideId != null
|
|
? 'Panduan berhasil diperbarui'
|
|
: 'Panduan baru berhasil ditambahkan',
|
|
),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error saving guide: $e');
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Error: ${e.toString()}'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setDialogState(
|
|
() => _isFormLoading = false,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primary,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: Text(
|
|
_currentGuideId != null ? 'Perbarui' : 'Simpan',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _deleteGuide(String id) async {
|
|
// Show confirmation dialog
|
|
final shouldDelete = await showDialog<bool>(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
title: const Text('Konfirmasi Hapus'),
|
|
content: const Text(
|
|
'Apakah Anda yakin ingin menghapus panduan ini? '
|
|
'Tindakan ini tidak dapat dibatalkan.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: const Text('Batal'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Hapus'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (shouldDelete != true) return;
|
|
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
// Delete guide from database
|
|
await _supabase.from('farming_guides').delete().eq('id', id);
|
|
|
|
// Reload guides
|
|
await _loadGuides();
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Panduan berhasil dihapus'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error deleting guide: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoading = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Error: ${e.toString()}'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Expanded(
|
|
child: Text(
|
|
'Manajemen Panduan Pertanian',
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: primaryGreen,
|
|
),
|
|
),
|
|
),
|
|
ElevatedButton.icon(
|
|
onPressed: () => _showAddEditGuideDialog(),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primary,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('Tambah Panduan'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
onChanged: _filterGuides,
|
|
decoration: InputDecoration(
|
|
hintText: 'Cari panduan...',
|
|
prefixIcon: const Icon(Icons.search),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Expanded(
|
|
child:
|
|
_isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _filteredGuides.isEmpty
|
|
? Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.menu_book,
|
|
size: 64,
|
|
color: Colors.grey[400],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
_searchQuery.isEmpty
|
|
? 'Belum ada panduan'
|
|
: 'Tidak ada panduan yang sesuai',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
if (_searchQuery.isNotEmpty)
|
|
TextButton(
|
|
onPressed: () {
|
|
setState(() => _searchQuery = '');
|
|
},
|
|
child: const Text('Tampilkan semua panduan'),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
itemCount: _filteredGuides.length,
|
|
itemBuilder: (context, index) {
|
|
final guide = _filteredGuides[index];
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (guide.imageUrl != null)
|
|
Builder(
|
|
builder: (context) {
|
|
// Perbaiki URL gambar menggunakan GuideService
|
|
final imageUrl =
|
|
_guideService.fixImageUrl(
|
|
guide.imageUrl,
|
|
) ??
|
|
'';
|
|
debugPrint('Guide image URL: $imageUrl');
|
|
|
|
return Container(
|
|
height: 150,
|
|
width: double.infinity,
|
|
decoration: const BoxDecoration(
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(8),
|
|
),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius:
|
|
const BorderRadius.vertical(
|
|
top: Radius.circular(8),
|
|
),
|
|
child:
|
|
imageUrl.isEmpty
|
|
? Container(
|
|
color: Colors.grey[200],
|
|
child: const Center(
|
|
child: Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment
|
|
.center,
|
|
children: [
|
|
Icon(
|
|
Icons
|
|
.image_not_supported,
|
|
),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'URL gambar tidak valid',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
: Image.network(
|
|
imageUrl,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (
|
|
context,
|
|
error,
|
|
stackTrace,
|
|
) {
|
|
// Log error untuk debugging
|
|
debugPrint(
|
|
'Error loading image: $error for URL $imageUrl',
|
|
);
|
|
return Container(
|
|
color: Colors.grey[200],
|
|
child: const Center(
|
|
child: Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment
|
|
.center,
|
|
children: [
|
|
Icon(
|
|
Icons
|
|
.error_outline,
|
|
),
|
|
SizedBox(
|
|
width: 8,
|
|
),
|
|
Text(
|
|
'Gagal memuat gambar',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
loadingBuilder: (
|
|
context,
|
|
child,
|
|
loadingProgress,
|
|
) {
|
|
if (loadingProgress ==
|
|
null)
|
|
return child;
|
|
return Container(
|
|
color: Colors.grey[200],
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
value:
|
|
loadingProgress
|
|
.expectedTotalBytes !=
|
|
null
|
|
? loadingProgress
|
|
.cumulativeBytesLoaded /
|
|
loadingProgress
|
|
.expectedTotalBytes!
|
|
: null,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
guide.title,
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: surfaceGreen,
|
|
borderRadius:
|
|
BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
guide.category,
|
|
style: const TextStyle(
|
|
color: primaryGreen,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
guide.content,
|
|
maxLines: 3,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Dibuat: ${guide.getFormattedDate()}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[500],
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
onPressed:
|
|
() =>
|
|
_showAddEditGuideDialog(
|
|
guide: guide,
|
|
),
|
|
icon: const Icon(
|
|
Icons.edit,
|
|
color: primaryGreen,
|
|
),
|
|
tooltip: 'Edit',
|
|
),
|
|
IconButton(
|
|
onPressed:
|
|
() =>
|
|
_deleteGuide(guide.id),
|
|
icon: const Icon(
|
|
Icons.delete,
|
|
color: Colors.red,
|
|
),
|
|
tooltip: 'Hapus',
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|