MIF_E31222656/lib/screens/community/components/simple_news_tab.dart

630 lines
19 KiB
Dart

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<SimpleNewsTab> {
bool _isLoading = true;
List<dynamic> _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<void> _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<void> _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<dynamic> 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<void> _saveNewsToCache(List<dynamic> 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<List<dynamic>> _filterWithGeminiAPI(List<dynamic> articles) async {
if (!mounted) return [];
List<dynamic> 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<void> _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<dynamic> 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<void> _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<void> _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),
),
),
),
],
);
}
}