251 lines
8.3 KiB
Dart
251 lines
8.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/models/farming_guide_model.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/data/static_guides_data.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/utils/plant_categorizer.dart';
|
|
|
|
class GuideService {
|
|
final _supabase = Supabase.instance.client;
|
|
|
|
// Storage bucket that should be used for images
|
|
String _imageBucket = 'images';
|
|
bool _bucketsChecked = false;
|
|
|
|
// Singleton pattern
|
|
static final GuideService _instance = GuideService._internal();
|
|
factory GuideService() => _instance;
|
|
GuideService._internal() {
|
|
// Check available buckets
|
|
_checkAvailableBuckets();
|
|
// Debug existing guides when service is created
|
|
debugCheckExistingGuides();
|
|
}
|
|
|
|
// Check available buckets and set the appropriate one to use
|
|
Future<void> _checkAvailableBuckets() async {
|
|
if (_bucketsChecked) return;
|
|
|
|
try {
|
|
final buckets = await _supabase.storage.listBuckets();
|
|
final bucketNames = buckets.map((b) => b.name).toList();
|
|
|
|
debugPrint('Available buckets: ${bucketNames.join(', ')}');
|
|
|
|
// Preferred bucket order: images, guide_images, avatars
|
|
if (bucketNames.contains('images')) {
|
|
_imageBucket = 'images';
|
|
} else if (bucketNames.contains('guide_images')) {
|
|
_imageBucket = 'guide_images';
|
|
} else if (bucketNames.contains('avatars')) {
|
|
_imageBucket = 'avatars';
|
|
} else if (bucketNames.isNotEmpty) {
|
|
_imageBucket = bucketNames.first;
|
|
}
|
|
|
|
debugPrint('Selected bucket for guide images: $_imageBucket');
|
|
_bucketsChecked = true;
|
|
} catch (e) {
|
|
debugPrint('Error checking buckets: $e');
|
|
}
|
|
}
|
|
|
|
// Debug function to check existing guides and their image URLs
|
|
Future<void> debugCheckExistingGuides() async {
|
|
try {
|
|
await _checkAvailableBuckets();
|
|
|
|
final response = await _supabase
|
|
.from('farming_guides')
|
|
.select('*')
|
|
.timeout(const Duration(seconds: 5));
|
|
|
|
debugPrint('====== DEBUG EXISTING GUIDES ======');
|
|
debugPrint('Found ${response.length} guides in database');
|
|
debugPrint('Using bucket: $_imageBucket');
|
|
|
|
// Check each guide and its image URL
|
|
for (var i = 0; i < response.length; i++) {
|
|
final guide = response[i];
|
|
final id = guide['id'] ?? 'unknown';
|
|
final title = guide['title'] ?? 'No title';
|
|
final imageUrl = guide['image_url'];
|
|
|
|
debugPrint('Guide #${i + 1}: $title (ID: $id)');
|
|
|
|
if (imageUrl == null) {
|
|
debugPrint(' - No image URL (null)');
|
|
} else if (imageUrl.isEmpty) {
|
|
debugPrint(' - Empty image URL');
|
|
} else {
|
|
debugPrint(' - Original image URL: $imageUrl');
|
|
final fixedUrl = fixImageUrl(imageUrl);
|
|
debugPrint(' - Fixed image URL: $fixedUrl');
|
|
|
|
// Try to fix the guide's image URL in the database if needed
|
|
if (fixedUrl != null && fixedUrl != imageUrl) {
|
|
try {
|
|
await _supabase
|
|
.from('farming_guides')
|
|
.update({'image_url': fixedUrl})
|
|
.eq('id', id);
|
|
debugPrint(' - Updated image URL in database');
|
|
} catch (e) {
|
|
debugPrint(' - Failed to update image URL: $e');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
debugPrint('====== END DEBUG ======');
|
|
} catch (e) {
|
|
debugPrint('Error checking existing guides: $e');
|
|
}
|
|
}
|
|
|
|
// Mengambil panduan dari database dan menggabungkan dengan data statis
|
|
Future<List<FarmingGuideModel>> getGuides() async {
|
|
try {
|
|
// Mencoba mengambil data dari Supabase dengan timeout 5 detik
|
|
final response = await _supabase
|
|
.from('farming_guides')
|
|
.select('*')
|
|
.order('created_at', ascending: false)
|
|
.timeout(const Duration(seconds: 5));
|
|
|
|
debugPrint('Loaded ${response.length} guides from database');
|
|
|
|
// Cek respons untuk masalah pada data gambar
|
|
for (final guide in response) {
|
|
if (guide['image_url'] == null) {
|
|
debugPrint('Guide with title "${guide['title']}" has null image_url');
|
|
} else {
|
|
debugPrint('Guide image URL: ${guide['image_url']}');
|
|
}
|
|
}
|
|
|
|
// Konversi ke List<FarmingGuideModel> dengan auto-kategorisasi jika perlu
|
|
final dbGuides =
|
|
List<Map<String, dynamic>>.from(response).map((map) {
|
|
// Jika kategori kosong atau generic, coba kategorikan otomatis
|
|
final currentCategory = map['category'] ?? '';
|
|
if (currentCategory.isEmpty ||
|
|
currentCategory.toLowerCase() == 'umum') {
|
|
final title = map['title'] ?? '';
|
|
final content = map['content'] ?? '';
|
|
|
|
// Auto-kategorisasi berdasarkan judul dan konten
|
|
final category = PlantCategorizer.categorize(
|
|
title,
|
|
description: content,
|
|
);
|
|
map['category'] = category;
|
|
|
|
debugPrint('Auto-categorized "${title}" as "$category"');
|
|
}
|
|
|
|
// Cek dan perbaiki URL gambar
|
|
if (map['image_url'] != null) {
|
|
map['image_url'] = fixImageUrl(map['image_url']);
|
|
}
|
|
|
|
return FarmingGuideModel.fromMap(map);
|
|
}).toList();
|
|
|
|
// Mendapatkan data statis
|
|
final staticGuides = StaticGuidesData().getAllGuides();
|
|
|
|
// Gabungkan keduanya
|
|
final allGuides = [...dbGuides, ...staticGuides];
|
|
|
|
// Hilangkan duplikat berdasarkan judul
|
|
final uniqueTitles = <String>{};
|
|
final uniqueGuides = <FarmingGuideModel>[];
|
|
|
|
for (final guide in allGuides) {
|
|
if (uniqueTitles.add(guide.title)) {
|
|
uniqueGuides.add(guide);
|
|
}
|
|
}
|
|
|
|
return uniqueGuides;
|
|
} catch (e) {
|
|
// Jika terjadi error, gunakan data statis saja
|
|
debugPrint('Error loading guides from database: $e');
|
|
debugPrint('Falling back to static data only');
|
|
return StaticGuidesData().getAllGuides();
|
|
}
|
|
}
|
|
|
|
// Mengambil panduan berdasarkan kategori
|
|
Future<List<FarmingGuideModel>> getGuidesByCategory(String category) async {
|
|
if (category.isEmpty) {
|
|
return getGuides();
|
|
}
|
|
|
|
try {
|
|
// Mendapatkan semua panduan terlebih dahulu
|
|
final allGuides = await getGuides();
|
|
|
|
// Filter berdasarkan kategori
|
|
// Ubah pencocokan menjadi case-insensitive dan juga menerima partial match
|
|
return allGuides.where((guide) {
|
|
// Check if the category matches (case insensitive)
|
|
if (guide.category.toLowerCase() == category.toLowerCase()) {
|
|
return true;
|
|
}
|
|
|
|
// Check if it's a partial match (for better categorization)
|
|
if (guide.category.toLowerCase().contains(category.toLowerCase()) ||
|
|
category.toLowerCase().contains(guide.category.toLowerCase())) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}).toList();
|
|
} catch (e) {
|
|
// Jika terjadi error, gunakan data statis saja
|
|
debugPrint('Error filtering guides by category: $e');
|
|
return StaticGuidesData().getGuidesByCategory(category);
|
|
}
|
|
}
|
|
|
|
// Memperbaiki URL gambar jika perlu
|
|
String? fixImageUrl(String? imageUrl) {
|
|
if (imageUrl == null || imageUrl.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
// Log untuk debugging
|
|
debugPrint('Original image URL: $imageUrl');
|
|
|
|
// Fix URL jika perlu (pastikan URL lengkap)
|
|
if (!imageUrl.startsWith('http')) {
|
|
// Jika URL tidak lengkap, gunakan Storage dari Supabase
|
|
String bucketName = _imageBucket; // Use the detected bucket
|
|
String fileName = imageUrl;
|
|
|
|
// Jika imageUrl sudah mengandung nama bucket, ekstrak
|
|
if (imageUrl.contains('/')) {
|
|
final parts = imageUrl.split('/');
|
|
if (parts.length >= 2) {
|
|
bucketName = parts[0];
|
|
fileName = parts.sublist(1).join('/');
|
|
}
|
|
}
|
|
|
|
// Dapatkan URL publik yang valid
|
|
try {
|
|
final fixedUrl = _supabase.storage
|
|
.from(bucketName)
|
|
.getPublicUrl(fileName);
|
|
debugPrint('Fixed image URL: $fixedUrl');
|
|
return fixedUrl;
|
|
} catch (e) {
|
|
debugPrint('Error fixing image URL: $e');
|
|
return imageUrl; // Kembalikan URL asli jika gagal
|
|
}
|
|
}
|
|
|
|
return imageUrl;
|
|
}
|
|
}
|