import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:tugas_akhir_supabase/core/theme/app_colors.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; class NewsManagement extends StatefulWidget { const NewsManagement({super.key}); @override State createState() => _NewsManagementState(); } class _NewsManagementState extends State { final _supabase = Supabase.instance.client; bool _isLoading = true; bool _isLoadingMore = false; // NewsAPI data String _apiKey = ''; List> _articles = []; List> _savedArticles = []; String _searchQuery = ''; String _currentCategory = 'agriculture'; int _currentPage = 1; bool _hasMorePages = true; // Form controllers final _apiKeyController = TextEditingController(); final _searchController = TextEditingController(); // Categories for agriculture news final List _categories = [ 'agriculture', 'farming', 'crops', 'organic farming', 'sustainable agriculture', 'agricultural technology', 'food production', ]; @override void initState() { super.initState(); _loadApiKey(); } @override void dispose() { _apiKeyController.dispose(); _searchController.dispose(); super.dispose(); } Future _loadApiKey() async { try { // Load API key from settings table final response = await _supabase .from('app_settings') .select() .eq('key', 'newsapi_key') .single(); if (mounted) { setState(() { _apiKey = response['value'] ?? ''; _apiKeyController.text = _apiKey; }); } // If we have an API key, load news if (_apiKey.isNotEmpty) { await _loadNews(); } // Load saved articles await _loadSavedArticles(); if (mounted) { setState(() => _isLoading = false); } } catch (e) { debugPrint('Error loading API key: $e'); // Create default entry if not exists try { await _supabase.from('app_settings').insert({ 'key': 'newsapi_key', 'value': '', 'description': 'API key for NewsAPI.org', }); } catch (insertError) { // Ignore if already exists debugPrint('Error creating API key setting: $insertError'); } if (mounted) { setState(() => _isLoading = false); } } } Future _saveApiKey() async { final newApiKey = _apiKeyController.text.trim(); if (newApiKey == _apiKey) return; setState(() => _isLoading = true); try { await _supabase .from('app_settings') .update({'value': newApiKey}) .eq('key', 'newsapi_key'); setState(() { _apiKey = newApiKey; _isLoading = false; }); // Reload news with new API key if (_apiKey.isNotEmpty) { _currentPage = 1; _articles = []; await _loadNews(); } ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('API key berhasil disimpan'), backgroundColor: Colors.green, ), ); } catch (e) { debugPrint('Error saving API key: $e'); setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } Future _loadNews() async { if (_apiKey.isEmpty) return; if (_currentPage == 1) { setState(() => _isLoading = true); } else { setState(() => _isLoadingMore = true); } try { final query = _searchQuery.isNotEmpty ? _searchQuery : _currentCategory; final url = Uri.parse( 'https://newsapi.org/v2/everything?q=$query&language=id&pageSize=10&page=$_currentPage&apiKey=$_apiKey', ); final response = await http.get(url); final data = json.decode(response.body); if (data['status'] == 'ok') { final articles = List>.from(data['articles']); // Check if we have more pages final totalResults = data['totalResults'] ?? 0; final hasMore = _currentPage * 10 < totalResults; setState(() { if (_currentPage == 1) { _articles = articles; } else { _articles.addAll(articles); } _hasMorePages = hasMore; _isLoading = false; _isLoadingMore = false; }); } else { throw Exception(data['message'] ?? 'Failed to load news'); } } catch (e) { debugPrint('Error loading news: $e'); setState(() { _isLoading = false; _isLoadingMore = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } Future _loadSavedArticles() async { try { final response = await _supabase .from('saved_news') .select('*') .order('published_at', ascending: false); final savedArticles = List>.from(response); if (mounted) { setState(() { _savedArticles = savedArticles; }); } } catch (e) { debugPrint('Error loading saved articles: $e'); // Try to create table if not exists try { // This would typically be done with a migration, but for simplicity await _supabase.rpc('create_saved_news_table_if_not_exists'); if (mounted) { setState(() { _savedArticles = []; }); } } catch (tableError) { debugPrint('Error creating saved_news table: $tableError'); } } } Future _saveArticle(Map article) async { try { // Check if already saved final exists = _savedArticles.any((a) => a['url'] == article['url']); if (exists) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Berita sudah disimpan sebelumnya'), backgroundColor: Colors.orange, ), ); return; } // Prepare data for saving final articleData = { 'title': article['title'], 'description': article['description'], 'url': article['url'], 'url_to_image': article['urlToImage'], 'published_at': article['publishedAt'], 'source_name': article['source']['name'], 'content': article['content'], 'is_featured': false, }; await _supabase.from('saved_news').insert(articleData); // Reload saved articles await _loadSavedArticles(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Berita berhasil disimpan'), backgroundColor: Colors.green, ), ); } catch (e) { debugPrint('Error saving article: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } Future _deleteArticle(int id) async { // Show confirmation dialog final shouldDelete = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Konfirmasi Hapus'), content: const Text( 'Apakah Anda yakin ingin menghapus berita 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; try { await _supabase.from('saved_news').delete().eq('id', id); // Reload saved articles await _loadSavedArticles(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Berita berhasil dihapus'), backgroundColor: Colors.green, ), ); } catch (e) { debugPrint('Error deleting article: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } Future _toggleFeatured(int id, bool currentValue) async { try { await _supabase .from('saved_news') .update({'is_featured': !currentValue}) .eq('id', id); // Reload saved articles await _loadSavedArticles(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( currentValue ? 'Berita dihapus dari featured' : 'Berita ditambahkan ke featured', ), backgroundColor: Colors.green, ), ); } catch (e) { debugPrint('Error toggling featured: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } void _showApiKeyDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Pengaturan NewsAPI'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _apiKeyController, decoration: const InputDecoration( labelText: 'API Key', border: OutlineInputBorder(), hintText: 'Masukkan NewsAPI key Anda', ), ), const SizedBox(height: 16), const Text( 'Dapatkan API key di newsapi.org', style: TextStyle(fontSize: 12, color: Colors.grey), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Batal'), ), ElevatedButton( onPressed: () { _saveApiKey(); Navigator.of(context).pop(); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, ), child: const Text('Simpan'), ), ], ), ); } void _searchNews() { _currentPage = 1; _articles = []; _searchQuery = _searchController.text.trim(); _loadNews(); } void _changeCategory(String category) { setState(() { _currentCategory = category; _searchQuery = ''; _searchController.clear(); _currentPage = 1; _articles = []; }); _loadNews(); } void _loadMoreNews() { if (!_isLoadingMore && _hasMorePages) { _currentPage++; _loadNews(); } } @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(48), child: AppBar( backgroundColor: Colors.transparent, elevation: 0, bottom: TabBar( tabs: const [ Tab(text: 'Pencarian Berita'), Tab(text: 'Berita Tersimpan'), ], labelColor: AppColors.primary, unselectedLabelColor: Colors.grey, indicatorColor: AppColors.primary, ), ), ), body: TabBarView( children: [ // Tab 1: News Search _buildNewsSearchTab(), // Tab 2: Saved News _buildSavedNewsTab(), ], ), ), ); } Widget _buildNewsSearchTab() { return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Cari berita pertanian...', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), contentPadding: const EdgeInsets.symmetric(vertical: 12), ), onSubmitted: (_) => _searchNews(), ), ), const SizedBox(width: 8), IconButton( onPressed: _showApiKeyDialog, icon: const Icon(Icons.settings), tooltip: 'Pengaturan API', ), ], ), const SizedBox(height: 16), // Categories SizedBox( height: 40, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _categories.length, itemBuilder: (context, index) { final category = _categories[index]; final isSelected = category == _currentCategory && _searchQuery.isEmpty; return Padding( padding: const EdgeInsets.only(right: 8), child: FilterChip( label: Text(category), selected: isSelected, onSelected: (_) => _changeCategory(category), backgroundColor: Colors.grey[200], selectedColor: AppColors.primary.withOpacity(0.2), labelStyle: TextStyle( color: isSelected ? AppColors.primary : Colors.black87, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ); }, ), ), const SizedBox(height: 16), // News list Expanded( child: _apiKey.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.vpn_key, size: 64, color: Colors.grey[400], ), const SizedBox(height: 16), const Text( 'API Key belum dikonfigurasi', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), const Text( 'Silakan tambahkan API Key NewsAPI untuk mulai', style: TextStyle(color: Colors.grey), ), const SizedBox(height: 16), ElevatedButton( onPressed: _showApiKeyDialog, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, ), child: const Text('Tambahkan API Key'), ), ], ), ) : _isLoading && _currentPage == 1 ? const Center(child: CircularProgressIndicator()) : _articles.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.article, size: 64, color: Colors.grey[400], ), const SizedBox(height: 16), const Text( 'Tidak ada berita ditemukan', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), const Text( 'Coba gunakan kata kunci pencarian yang berbeda', style: TextStyle(color: Colors.grey), ), ], ), ) : NotificationListener( onNotification: (ScrollNotification scrollInfo) { if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) { _loadMoreNews(); return true; } return false; }, child: ListView.builder( itemCount: _articles.length + (_hasMorePages ? 1 : 0), itemBuilder: (context, index) { if (index == _articles.length) { return _isLoadingMore ? const Center( child: Padding( padding: EdgeInsets.all(16.0), child: CircularProgressIndicator(), ), ) : const SizedBox.shrink(); } final article = _articles[index]; return _buildNewsCard(article, true); }, ), ), ), ], ), ); } Widget _buildSavedNewsTab() { return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Berita Tersimpan', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: AppColors.primary, ), ), const SizedBox(height: 16), Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : _savedArticles.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.article, size: 64, color: Colors.grey[400], ), const SizedBox(height: 16), const Text( 'Belum ada berita tersimpan', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), const Text( 'Simpan berita dari tab pencarian', style: TextStyle(color: Colors.grey), ), ], ), ) : ListView.builder( itemCount: _savedArticles.length, itemBuilder: (context, index) { final article = _savedArticles[index]; return _buildSavedNewsCard(article); }, ), ), ], ), ); } Widget _buildNewsCard(Map article, bool isSearchResult) { final title = article['title'] ?? 'Tanpa Judul'; final description = article['description'] ?? 'Tidak ada deskripsi'; final imageUrl = article['urlToImage']; final source = article['source']?['name'] ?? 'Unknown Source'; final publishedAt = _formatDate(article['publishedAt']); return Card( margin: const EdgeInsets.only(bottom: 16), elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (imageUrl != null) ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(8), ), child: Image.network( imageUrl, height: 180, width: double.infinity, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container( height: 180, color: Colors.grey[300], child: const Center( child: Icon( Icons.broken_image, size: 64, color: Colors.white70, ), ), ), ), ), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Text( description, maxLines: 3, overflow: TextOverflow.ellipsis, style: TextStyle(color: Colors.grey[600]), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( source, style: const TextStyle( fontWeight: FontWeight.bold, color: AppColors.primary, ), ), Text( publishedAt, style: TextStyle( fontSize: 12, color: Colors.grey[500], ), ), ], ), if (isSearchResult) ElevatedButton.icon( onPressed: () => _saveArticle(article), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, ), icon: const Icon(Icons.save), label: const Text('Simpan'), ), ], ), ], ), ), ], ), ); } Widget _buildSavedNewsCard(Map article) { final id = article['id']; final title = article['title'] ?? 'Tanpa Judul'; final description = article['description'] ?? 'Tidak ada deskripsi'; final imageUrl = article['url_to_image']; final source = article['source_name'] ?? 'Unknown Source'; final publishedAt = _formatDate(article['published_at']); final isFeatured = article['is_featured'] ?? false; return Card( margin: const EdgeInsets.only(bottom: 16), elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (imageUrl != null) Stack( children: [ ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(8), ), child: Image.network( imageUrl, height: 180, width: double.infinity, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container( height: 180, color: Colors.grey[300], child: const Center( child: Icon( Icons.broken_image, size: 64, color: Colors.white70, ), ), ), ), ), if (isFeatured) Positioned( top: 8, right: 8, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(4), ), child: const Text( 'Featured', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12, ), ), ), ), ], ), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Text( description, maxLines: 3, overflow: TextOverflow.ellipsis, style: TextStyle(color: Colors.grey[600]), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( source, style: const TextStyle( fontWeight: FontWeight.bold, color: AppColors.primary, ), ), Text( publishedAt, style: TextStyle( fontSize: 12, color: Colors.grey[500], ), ), ], ), Row( children: [ IconButton( onPressed: () => _toggleFeatured(id, isFeatured), icon: Icon( isFeatured ? Icons.star : Icons.star_border, color: isFeatured ? Colors.amber : Colors.grey, ), tooltip: isFeatured ? 'Hapus dari Featured' : 'Jadikan Featured', ), IconButton( onPressed: () => _deleteArticle(id), icon: const Icon(Icons.delete, color: Colors.red), tooltip: 'Hapus', ), ], ), ], ), ], ), ), ], ), ); } String _formatDate(String? dateStr) { if (dateStr == null) return 'N/A'; try { final date = DateTime.parse(dateStr); return '${date.day}/${date.month}/${date.year}'; } catch (e) { return 'N/A'; } } }