import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'dart:math'; import 'package:url_launcher/url_launcher.dart'; import 'package:tugas_akhir_supabase/screens/community/components/simple_news_card.dart'; import 'package:tugas_akhir_supabase/core/theme/app_colors.dart'; import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:tugas_akhir_supabase/screens/community/components/news_web_view.dart'; import 'package:tugas_akhir_supabase/services/gemini_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SimpleNewsTab extends StatefulWidget { const SimpleNewsTab({super.key}); @override _SimpleNewsTabState createState() => _SimpleNewsTabState(); } class _SimpleNewsTabState extends State { bool _isLoading = true; List _newsArticles = []; String? _errorMessage; int _currentPage = 1; bool _hasMoreData = true; final ScrollController _scrollController = ScrollController(); int _totalResults = 0; final int _pageSize = 10; bool _isFiltering = false; // Konstanta untuk caching static const String _cachedNewsKey = 'cached_farming_news'; static const String _lastFetchTimeKey = 'last_news_fetch_time'; @override void initState() { super.initState(); _loadNewsWithCache(); _scrollController.addListener(_scrollListener); } @override void dispose() { _scrollController.removeListener(_scrollListener); _scrollController.dispose(); super.dispose(); } void _scrollListener() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { if (_hasMoreData && !_isLoading) { _loadMoreNews(); } } } // Metode baru untuk load berita dengan caching Future _loadNewsWithCache() async { if (!mounted) return; setState(() { _isLoading = true; }); try { final prefs = await SharedPreferences.getInstance(); final lastFetchTime = prefs.getInt(_lastFetchTimeKey); final currentTime = DateTime.now().millisecondsSinceEpoch; // Cek apakah cache masih valid (kurang dari 24 jam) final cacheValid = lastFetchTime != null && currentTime - lastFetchTime < 24 * 60 * 60 * 1000; // 24 jam dalam milidetik if (cacheValid) { // Ambil berita dari cache final cachedNewsJson = prefs.getString(_cachedNewsKey); if (cachedNewsJson != null && cachedNewsJson.isNotEmpty) { final cachedNews = json.decode(cachedNewsJson); if (!mounted) return; setState(() { _newsArticles = cachedNews; _isLoading = false; _errorMessage = null; }); debugPrint('Loaded ${_newsArticles.length} news articles from cache'); return; } } // Jika cache tidak valid atau kosong, load berita baru if (mounted) { await _loadNews(); } } catch (e) { debugPrint('Error loading cached news: $e'); // Fallback ke load berita normal jika ada error if (mounted) { await _loadNews(); } } } Future _loadNews() async { if (!mounted) return; setState(() { _isLoading = true; _errorMessage = null; _currentPage = 1; }); try { // API Key for NewsAPI.org final String apiKey = '75571d40e2d743bc837012edce849d98'; // Get the date 30 days ago (sesuai batasan free plan) final DateTime now = DateTime.now(); final DateTime oneMonthAgo = now.subtract(Duration(days: 30)); final String fromDate = DateFormat('yyyy-MM-dd').format(oneMonthAgo); // Build URL with parameters for general agriculture news final url = Uri.parse('https://newsapi.org/v2/everything').replace( queryParameters: { 'q': 'pertanian OR petani OR budidaya OR tanaman', 'language': 'id', 'from': fromDate, 'sortBy': 'publishedAt', 'pageSize': '20', 'page': '1', 'apiKey': apiKey, }, ); debugPrint('Making API request to: $url'); // Make API request final response = await http.get(url); // Check if widget is still mounted before proceeding if (!mounted) return; // Debug response debugPrint('API Response Status: ${response.statusCode}'); debugPrint( 'API Response Body: ${response.body.substring(0, min(500, response.body.length))}...', ); // Check response status if (response.statusCode == 200) { final jsonData = json.decode(response.body); debugPrint('Response status: ${jsonData['status']}'); debugPrint('Total results: ${jsonData['totalResults']}'); if (jsonData['status'] == 'ok' && jsonData['articles'] != null && jsonData['articles'] is List) { List articles = jsonData['articles']; // Filter with Gemini API if (!mounted) return; setState(() { _isFiltering = true; }); final filteredArticles = await _filterWithGeminiAPI(articles); // Check if widget is still mounted if (!mounted) return; setState(() { _newsArticles = filteredArticles; _isLoading = false; _isFiltering = false; _totalResults = jsonData['totalResults'] ?? 0; _hasMoreData = _newsArticles.length < _totalResults; }); // Simpan hasil ke cache if (mounted) { _saveNewsToCache(filteredArticles); } } else { if (!mounted) return; setState(() { _errorMessage = 'Tidak ada artikel pertanian yang ditemukan'; _isLoading = false; _newsArticles = []; }); } } else { final jsonData = json.decode(response.body); if (!mounted) return; setState(() { _errorMessage = 'Gagal memuat berita: ${response.statusCode}\n${jsonData['message'] ?? response.body}'; _isLoading = false; }); } } catch (e, stackTrace) { debugPrint('Error loading news: $e'); debugPrint('Stack trace: $stackTrace'); if (!mounted) return; setState(() { _errorMessage = 'Terjadi kesalahan: $e'; _isLoading = false; }); } } // Metode untuk menyimpan berita ke cache Future _saveNewsToCache(List news) async { try { final prefs = await SharedPreferences.getInstance(); final newsJson = json.encode(news); final currentTime = DateTime.now().millisecondsSinceEpoch; await prefs.setString(_cachedNewsKey, newsJson); await prefs.setInt(_lastFetchTimeKey, currentTime); debugPrint('Saved ${news.length} news articles to cache'); } catch (e) { debugPrint('Error saving news to cache: $e'); } } // Filter articles using Gemini API Future> _filterWithGeminiAPI(List articles) async { if (!mounted) return []; List filteredArticles = []; // Process in batches to avoid overloading final batchSize = 5; for (int i = 0; i < articles.length; i += batchSize) { // Check if widget is still mounted before processing each batch if (!mounted) return filteredArticles; final end = (i + batchSize < articles.length) ? i + batchSize : articles.length; final batch = articles.sublist(i, end); // Process each article in the batch for (final article in batch) { // Check if widget is still mounted before processing each article if (!mounted) return filteredArticles; final title = article['title'] ?? ''; final description = article['description'] ?? ''; // Combine title and description for better context final content = '$title. $description'; try { // Ask Gemini if this is useful farming content final prompt = "Apakah teks berikut berisi tips atau informasi bermanfaat tentang pertanian? Jawab hanya dengan 'ya' atau 'tidak'. Teks: '$content'"; final result = await GeminiService.askGemini(prompt); // Check if Gemini thinks it's relevant if (result.toLowerCase().contains('ya')) { filteredArticles.add(article); } } catch (e) { debugPrint('Error filtering with Gemini: $e'); // If Gemini fails, include the article by default filteredArticles.add(article); } // Small delay to avoid rate limiting await Future.delayed(Duration(milliseconds: 200)); } } // If filtering removed too many articles, return some original ones if (filteredArticles.length < 3 && articles.isNotEmpty) { return articles.take(5).toList(); } return filteredArticles; } Future _loadMoreNews() async { if (_isLoading || !_hasMoreData || !mounted) return; setState(() { _isLoading = true; }); try { final String apiKey = '75571d40e2d743bc837012edce849d98'; final nextPage = _currentPage + 1; // Get the date 30 days ago (sesuai batasan free plan) final DateTime now = DateTime.now(); final DateTime oneMonthAgo = now.subtract(Duration(days: 30)); final String fromDate = DateFormat('yyyy-MM-dd').format(oneMonthAgo); final url = Uri.parse('https://newsapi.org/v2/everything').replace( queryParameters: { 'q': 'pertanian OR petani OR budidaya OR tanaman', 'language': 'id', 'from': fromDate, 'sortBy': 'publishedAt', 'pageSize': '20', 'page': nextPage.toString(), 'apiKey': apiKey, }, ); final response = await http.get(url); // Check if widget is still mounted if (!mounted) return; if (response.statusCode == 200) { final jsonData = json.decode(response.body); if (jsonData['status'] == 'ok' && jsonData['articles'] != null && jsonData['articles'] is List) { List articles = jsonData['articles']; // Filter with Gemini API final filteredArticles = await _filterWithGeminiAPI(articles); // Check if widget is still mounted if (!mounted) return; setState(() { _newsArticles.addAll(filteredArticles); _currentPage = nextPage; _hasMoreData = _newsArticles.length < (_totalResults ?? 0); _isLoading = false; }); // Update cache with new articles if (mounted) { _saveNewsToCache(_newsArticles); } } else { if (!mounted) return; setState(() { _hasMoreData = false; _isLoading = false; }); } } else { if (!mounted) return; setState(() { _hasMoreData = false; _isLoading = false; }); } } catch (e) { if (!mounted) return; setState(() { _isLoading = false; }); } } // Metode untuk memaksa refresh berita (untuk tombol refresh) Future _forceRefresh() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_lastFetchTimeKey); if (mounted) { await _loadNews(); } } catch (e) { debugPrint('Error during force refresh: $e'); } } Future _launchURL(String url, String title) async { if (!mounted) return; if (url.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('URL tidak tersedia untuk artikel ini')), ); return; } try { debugPrint('Attempting to open URL: $url'); // Use in-app WebView Navigator.push( context, MaterialPageRoute( builder: (context) => NewsWebView(url: url, title: title), ), ); } catch (e) { debugPrint('Error opening URL: $e'); // Fallback to external browser if WebView fails try { final uri = Uri.parse(url); if (!mounted) return; if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Tidak dapat membuka URL: $url')), ); } } catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Error: $e'))); } } } @override Widget build(BuildContext context) { return Column( children: [ // Header for Farming Tips Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Berita Pertanian', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.primary, ), ), SizedBox(height: 2), Text( 'Tips dan panduan untuk meningkatkan hasil pertanian', style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), ], ), ), Divider(height: 1), // Tab content Expanded( child: RefreshIndicator( onRefresh: _forceRefresh, // Gunakan force refresh untuk pull-to-refresh child: _isLoading && _newsArticles.isEmpty ? _buildLoadingView() : _errorMessage != null ? _buildErrorView() : _newsArticles.isEmpty ? _buildEmptyState() : _buildNewsListView(), ), ), ], ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.eco_outlined, size: 64, color: Colors.grey[400]), SizedBox(height: 16), Text( 'Belum ada tips pertanian', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey[700], ), ), SizedBox(height: 8), Text( 'Tips dan panduan bertani akan muncul di sini', textAlign: TextAlign.center, style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), SizedBox(height: 24), ElevatedButton.icon( onPressed: _forceRefresh, // Gunakan force refresh untuk tombol refresh style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), icon: Icon(Icons.refresh), label: Text('Muat Ulang'), ), ], ), ); } Widget _buildErrorView() { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 48, color: Colors.red), SizedBox(height: 16), Text( 'Gagal memuat berita pertanian', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), SizedBox(height: 8), Text( _errorMessage ?? 'Terjadi kesalahan tidak diketahui', style: TextStyle(color: Colors.red[700]), textAlign: TextAlign.center, ), SizedBox(height: 24), ElevatedButton.icon( onPressed: _forceRefresh, // Gunakan force refresh untuk tombol coba lagi style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), icon: Icon(Icons.refresh), label: Text('Coba Lagi'), ), ], ), ), ); } Widget _buildLoadingView() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: AppColors.primary), SizedBox(height: 16), Text( _isFiltering ? 'Memfilter konten pertanian bermanfaat...' : 'Memuat informasi pertanian...', style: TextStyle(fontSize: 16, color: Colors.grey[700]), ), ], ), ); } Widget _buildNewsListView() { return Stack( children: [ ListView.builder( controller: _scrollController, padding: EdgeInsets.only(top: 8, bottom: 16), itemCount: _newsArticles.length + (_hasMoreData ? 1 : 0), itemBuilder: (context, index) { if (index == _newsArticles.length) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: CircularProgressIndicator(color: AppColors.primary), ), ); } final article = _newsArticles[index]; final title = article['title'] ?? 'Judul tidak tersedia'; final content = article['description'] ?? 'Konten tidak tersedia'; final source = article['source']?['name'] ?? 'Sumber tidak diketahui'; final imageUrl = article['urlToImage']; // Ensure URL is properly formatted String url = article['url'] ?? ''; if (url.isNotEmpty && !url.startsWith('http')) { url = 'https://$url'; } final date = article['publishedAt'] != null ? DateTime.tryParse(article['publishedAt']) : null; return SimpleNewsCard( title: title, content: content, source: source, imageUrl: imageUrl, onTap: () => _launchURL(url, title), publishDate: date, ); }, ), if (_isLoading && _newsArticles.isNotEmpty) Positioned( bottom: 0, left: 0, right: 0, child: Container( color: Colors.white.withOpacity(0.7), padding: EdgeInsets.all(8.0), child: Center( child: CircularProgressIndicator(color: AppColors.primary), ), ), ), ], ); } }