// lib/ui/pinepods/home.dart import 'package:flutter/material.dart'; import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart'; import 'package:pinepods_mobile/bloc/podcast/podcast_bloc.dart'; import 'package:pinepods_mobile/services/pinepods/pinepods_service.dart'; import 'package:pinepods_mobile/services/pinepods/pinepods_audio_service.dart'; import 'package:pinepods_mobile/services/audio/audio_player_service.dart'; import 'package:pinepods_mobile/services/global_services.dart'; import 'package:pinepods_mobile/entities/home_data.dart'; import 'package:pinepods_mobile/entities/pinepods_episode.dart'; import 'package:pinepods_mobile/entities/episode.dart'; import 'package:pinepods_mobile/ui/pinepods/feed.dart'; import 'package:pinepods_mobile/ui/pinepods/saved.dart'; import 'package:pinepods_mobile/ui/pinepods/downloads.dart'; import 'package:pinepods_mobile/ui/pinepods/queue.dart'; import 'package:pinepods_mobile/ui/pinepods/history.dart'; import 'package:pinepods_mobile/ui/pinepods/playlists.dart'; import 'package:pinepods_mobile/ui/pinepods/playlist_episodes.dart'; import 'package:pinepods_mobile/ui/pinepods/episode_details.dart'; import 'package:pinepods_mobile/ui/pinepods/podcast_details.dart'; import 'package:pinepods_mobile/entities/pinepods_search.dart'; import 'package:pinepods_mobile/ui/widgets/episode_context_menu.dart'; import 'package:pinepods_mobile/ui/utils/player_utils.dart'; import 'package:pinepods_mobile/ui/widgets/server_error_page.dart'; import 'package:pinepods_mobile/services/error_handling_service.dart'; import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; class PinepodsHome extends StatefulWidget { const PinepodsHome({Key? key}) : super(key: key); @override State createState() => _PinepodsHomeState(); } class _PinepodsHomeState extends State { bool _isLoading = true; String _errorMessage = ''; HomeOverview? _homeData; PlaylistResponse? _playlistData; final PinepodsService _pinepodsService = PinepodsService(); // Use global audio service instead of creating local instance int? _contextMenuEpisodeIndex; bool _isContextMenuForContinueListening = false; @override void initState() { super.initState(); _loadHomeContent(); } Future _loadHomeContent() async { final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; if (settings.pinepodsServer == null || settings.pinepodsApiKey == null || settings.pinepodsUserId == null) { setState(() { _errorMessage = 'Not connected to PinePods server. Please connect in Settings.'; _isLoading = false; }); return; } setState(() { _isLoading = true; _errorMessage = ''; }); try { _pinepodsService.setCredentials( settings.pinepodsServer!, settings.pinepodsApiKey!, ); GlobalServices.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); // Load home data and playlists in parallel final futures = await Future.wait([ _pinepodsService.getHomeOverview(settings.pinepodsUserId!), _pinepodsService.getPlaylists(settings.pinepodsUserId!), ]); setState(() { _homeData = futures[0] as HomeOverview; _playlistData = futures[1] as PlaylistResponse; _isLoading = false; }); } catch (e) { setState(() { _errorMessage = 'Error loading home content: $e'; _isLoading = false; }); } } PinepodsAudioService? get _audioService => GlobalServices.pinepodsAudioService; Future _playEpisode(HomeEpisode homeEpisode) async { if (_audioService == null) { _showSnackBar('Audio service not available', Colors.red); return; } // Convert HomeEpisode to PinepodsEpisode final episode = PinepodsEpisode( podcastName: homeEpisode.podcastName, episodeTitle: homeEpisode.episodeTitle, episodePubDate: homeEpisode.episodePubDate, episodeDescription: homeEpisode.episodeDescription ?? '', episodeArtwork: homeEpisode.episodeArtwork, episodeUrl: homeEpisode.episodeUrl, episodeDuration: homeEpisode.episodeDuration, listenDuration: homeEpisode.listenDuration, episodeId: homeEpisode.episodeId, completed: homeEpisode.completed, saved: homeEpisode.saved, queued: homeEpisode.queued, downloaded: homeEpisode.downloaded, isYoutube: homeEpisode.isYoutube, ); try { await playPinepodsEpisodeWithOptionalFullScreen( context, _audioService!, episode, ); } catch (e) { if (mounted) { _showSnackBar('Failed to play episode: $e', Colors.red); } } } void _showContextMenu(int episodeIndex, bool isContinueListening) { setState(() { _contextMenuEpisodeIndex = episodeIndex; _isContextMenuForContinueListening = isContinueListening; }); } void _hideContextMenu() { setState(() { _contextMenuEpisodeIndex = null; _isContextMenuForContinueListening = false; }); } Future _saveEpisode(int episodeIndex, bool isContinueListening) async { final episodes = isContinueListening ? _homeData!.inProgressEpisodes : _homeData!.recentEpisodes; final homeEpisode = episodes[episodeIndex]; final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; final userId = settings.pinepodsUserId; if (userId == null) { _showSnackBar('Not logged in', Colors.red); return; } _pinepodsService.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); GlobalServices.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); try { final success = await _pinepodsService.saveEpisode( homeEpisode.episodeId, userId, homeEpisode.isYoutube, ); if (success) { // Update the local state setState(() { if (isContinueListening) { _homeData!.inProgressEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, saved: true); } else { _homeData!.recentEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, saved: true); } }); _showSnackBar('Episode saved!', Colors.green); } else { _showSnackBar('Failed to save episode', Colors.red); } } catch (e) { _showSnackBar('Error saving episode: $e', Colors.red); } } Future _removeSavedEpisode(int episodeIndex, bool isContinueListening) async { final episodes = isContinueListening ? _homeData!.inProgressEpisodes : _homeData!.recentEpisodes; final homeEpisode = episodes[episodeIndex]; final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; final userId = settings.pinepodsUserId; if (userId == null) { _showSnackBar('Not logged in', Colors.red); return; } _pinepodsService.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); GlobalServices.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); try { final success = await _pinepodsService.removeSavedEpisode( homeEpisode.episodeId, userId, homeEpisode.isYoutube, ); if (success) { setState(() { if (isContinueListening) { _homeData!.inProgressEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, saved: false); } else { _homeData!.recentEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, saved: false); } }); _showSnackBar('Removed from saved episodes', Colors.orange); } else { _showSnackBar('Failed to remove saved episode', Colors.red); } } catch (e) { _showSnackBar('Error removing saved episode: $e', Colors.red); } } Future _downloadEpisode(int episodeIndex, bool isContinueListening) async { final episodes = isContinueListening ? _homeData!.inProgressEpisodes : _homeData!.recentEpisodes; final homeEpisode = episodes[episodeIndex]; final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; final userId = settings.pinepodsUserId; if (userId == null) { _showSnackBar('Not logged in', Colors.red); return; } _pinepodsService.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); GlobalServices.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); try { final success = await _pinepodsService.downloadEpisode( homeEpisode.episodeId, userId, homeEpisode.isYoutube, ); if (success) { setState(() { if (isContinueListening) { _homeData!.inProgressEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, downloaded: true); } else { _homeData!.recentEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, downloaded: true); } }); _showSnackBar('Episode download queued!', Colors.green); } else { _showSnackBar('Failed to queue download', Colors.red); } } catch (e) { _showSnackBar('Error downloading episode: $e', Colors.red); } } Future _localDownloadEpisode(int episodeIndex, bool isContinueListening) async { final episodes = isContinueListening ? _homeData!.inProgressEpisodes : _homeData!.recentEpisodes; final homeEpisode = episodes[episodeIndex]; try { // Convert HomeEpisode to Episode for local download final localEpisode = Episode( guid: 'pinepods_${homeEpisode.episodeId}_${DateTime.now().millisecondsSinceEpoch}', pguid: 'pinepods_${homeEpisode.podcastName.replaceAll(' ', '_').toLowerCase()}', podcast: homeEpisode.podcastName, title: homeEpisode.episodeTitle, description: homeEpisode.episodeDescription, imageUrl: homeEpisode.episodeArtwork, contentUrl: homeEpisode.episodeUrl, duration: homeEpisode.episodeDuration, publicationDate: DateTime.tryParse(homeEpisode.episodePubDate), author: homeEpisode.podcastName, season: 0, episode: 0, position: homeEpisode.listenDuration ?? 0, played: homeEpisode.completed, chapters: [], transcriptUrls: [], ); final podcastBloc = Provider.of(context, listen: false); // First save the episode to the repository so it can be tracked await podcastBloc.podcastService.saveEpisode(localEpisode); // Use the download service from podcast bloc final success = await podcastBloc.downloadService.downloadEpisode(localEpisode); if (success) { _showSnackBar('Episode download started', Colors.green); } else { _showSnackBar('Failed to start download', Colors.red); } } catch (e) { _showSnackBar('Error starting local download: $e', Colors.red); } _hideContextMenu(); } Future _deleteEpisode(int episodeIndex, bool isContinueListening) async { final episodes = isContinueListening ? _homeData!.inProgressEpisodes : _homeData!.recentEpisodes; final homeEpisode = episodes[episodeIndex]; final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; final userId = settings.pinepodsUserId; if (userId == null) { _showSnackBar('Not logged in', Colors.red); return; } _pinepodsService.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); GlobalServices.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); try { final success = await _pinepodsService.deleteEpisode( homeEpisode.episodeId, userId, homeEpisode.isYoutube, ); if (success) { setState(() { if (isContinueListening) { _homeData!.inProgressEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, downloaded: false); } else { _homeData!.recentEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, downloaded: false); } }); _showSnackBar('Episode deleted from server', Colors.orange); } else { _showSnackBar('Failed to delete episode', Colors.red); } } catch (e) { _showSnackBar('Error deleting episode: $e', Colors.red); } } Future _toggleQueueEpisode(int episodeIndex, bool isContinueListening) async { final episodes = isContinueListening ? _homeData!.inProgressEpisodes : _homeData!.recentEpisodes; final homeEpisode = episodes[episodeIndex]; final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; final userId = settings.pinepodsUserId; if (userId == null) { _showSnackBar('Not logged in', Colors.red); return; } _pinepodsService.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); GlobalServices.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); try { bool success; if (homeEpisode.queued) { success = await _pinepodsService.removeQueuedEpisode( homeEpisode.episodeId, userId, homeEpisode.isYoutube, ); if (success) { setState(() { if (isContinueListening) { _homeData!.inProgressEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, queued: false); } else { _homeData!.recentEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, queued: false); } }); _showSnackBar('Removed from queue', Colors.orange); } } else { success = await _pinepodsService.queueEpisode( homeEpisode.episodeId, userId, homeEpisode.isYoutube, ); if (success) { setState(() { if (isContinueListening) { _homeData!.inProgressEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, queued: true); } else { _homeData!.recentEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, queued: true); } }); _showSnackBar('Added to queue!', Colors.green); } } if (!success) { _showSnackBar('Failed to update queue', Colors.red); } } catch (e) { _showSnackBar('Error updating queue: $e', Colors.red); } } Future _toggleMarkComplete(int episodeIndex, bool isContinueListening) async { final episodes = isContinueListening ? _homeData!.inProgressEpisodes : _homeData!.recentEpisodes; final homeEpisode = episodes[episodeIndex]; final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; final userId = settings.pinepodsUserId; if (userId == null) { _showSnackBar('Not logged in', Colors.red); return; } _pinepodsService.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); GlobalServices.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); try { bool success; if (homeEpisode.completed) { success = await _pinepodsService.markEpisodeUncompleted( homeEpisode.episodeId, userId, homeEpisode.isYoutube, ); if (success) { setState(() { if (isContinueListening) { _homeData!.inProgressEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, completed: false); } else { _homeData!.recentEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, completed: false); } }); _showSnackBar('Marked as incomplete', Colors.orange); } } else { success = await _pinepodsService.markEpisodeCompleted( homeEpisode.episodeId, userId, homeEpisode.isYoutube, ); if (success) { setState(() { if (isContinueListening) { _homeData!.inProgressEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, completed: true); } else { _homeData!.recentEpisodes[episodeIndex] = _updateHomeEpisodeProperty(homeEpisode, completed: true); } }); _showSnackBar('Marked as complete!', Colors.green); } } if (!success) { _showSnackBar('Failed to update completion status', Colors.red); } } catch (e) { _showSnackBar('Error updating completion: $e', Colors.red); } } HomeEpisode _updateHomeEpisodeProperty( HomeEpisode episode, { bool? saved, bool? downloaded, bool? queued, bool? completed, }) { return HomeEpisode( episodeId: episode.episodeId, podcastId: episode.podcastId, episodeTitle: episode.episodeTitle, episodeDescription: episode.episodeDescription, episodeUrl: episode.episodeUrl, episodeArtwork: episode.episodeArtwork, episodePubDate: episode.episodePubDate, episodeDuration: episode.episodeDuration, completed: completed ?? episode.completed, podcastName: episode.podcastName, isYoutube: episode.isYoutube, listenDuration: episode.listenDuration, saved: saved ?? episode.saved, queued: queued ?? episode.queued, downloaded: downloaded ?? episode.downloaded, ); } void _showSnackBar(String message, Color backgroundColor) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: backgroundColor, duration: const Duration(seconds: 2), ), ); } } @override Widget build(BuildContext context) { // Show context menu as a modal overlay if needed if (_contextMenuEpisodeIndex != null) { final episodeIndex = _contextMenuEpisodeIndex!; final episodes = _isContextMenuForContinueListening ? (_homeData?.inProgressEpisodes ?? []) : (_homeData?.recentEpisodes ?? []); if (episodeIndex < episodes.length) { final homeEpisode = episodes[episodeIndex]; final episode = PinepodsEpisode( podcastName: homeEpisode.podcastName, episodeTitle: homeEpisode.episodeTitle, episodePubDate: homeEpisode.episodePubDate, episodeDescription: homeEpisode.episodeDescription ?? '', episodeArtwork: homeEpisode.episodeArtwork, episodeUrl: homeEpisode.episodeUrl, episodeDuration: homeEpisode.episodeDuration, listenDuration: homeEpisode.listenDuration, episodeId: homeEpisode.episodeId, completed: homeEpisode.completed, saved: homeEpisode.saved, queued: homeEpisode.queued, downloaded: homeEpisode.downloaded, isYoutube: homeEpisode.isYoutube, ); WidgetsBinding.instance.addPostFrameCallback((_) { showDialog( context: context, barrierColor: Colors.black.withOpacity(0.3), builder: (context) => EpisodeContextMenu( episode: episode, onSave: episode.saved ? null : () { Navigator.of(context).pop(); _saveEpisode(episodeIndex, _isContextMenuForContinueListening); }, onRemoveSaved: episode.saved ? () { Navigator.of(context).pop(); _removeSavedEpisode(episodeIndex, _isContextMenuForContinueListening); } : null, onDownload: episode.downloaded ? () { Navigator.of(context).pop(); _deleteEpisode(episodeIndex, _isContextMenuForContinueListening); } : () { Navigator.of(context).pop(); _downloadEpisode(episodeIndex, _isContextMenuForContinueListening); }, onLocalDownload: () { Navigator.of(context).pop(); _localDownloadEpisode(episodeIndex, _isContextMenuForContinueListening); }, onQueue: () { Navigator.of(context).pop(); _toggleQueueEpisode(episodeIndex, _isContextMenuForContinueListening); }, onMarkComplete: () { Navigator.of(context).pop(); _toggleMarkComplete(episodeIndex, _isContextMenuForContinueListening); }, onDismiss: () { Navigator.of(context).pop(); _hideContextMenu(); }, ), ); }); } // Reset the context menu index after storing it locally _contextMenuEpisodeIndex = null; } return SliverList( delegate: SliverChildListDelegate([ if (_isLoading) const Padding( padding: EdgeInsets.all(32.0), child: Center( child: Column( children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Loading your podcasts...'), ], ), ), ) else if (_errorMessage.isNotEmpty) ServerErrorPage( errorMessage: _errorMessage.isServerConnectionError ? null : _errorMessage, onRetry: _loadHomeContent, title: 'Home Unavailable', subtitle: _errorMessage.isServerConnectionError ? 'Unable to connect to the PinePods server' : 'Failed to load home content', ) else if (_homeData != null) Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Stats Overview Section _buildStatsSection(), const SizedBox(height: 24), // Continue Listening Section if (_homeData!.inProgressEpisodes.isNotEmpty) ...[ _buildContinueListeningSection(), const SizedBox(height: 24), ], // Top Podcasts Section if (_homeData!.topPodcasts.isNotEmpty) ...[ _buildTopPodcastsSection(), const SizedBox(height: 24), ], // Smart Playlists Section if (_playlistData?.playlists.isNotEmpty == true) ...[ _buildPlaylistsSection(), const SizedBox(height: 24), ], // Recent Episodes Section if (_homeData!.recentEpisodes.isNotEmpty) ...[ _buildRecentEpisodesSection(), const SizedBox(height: 24), ], // Empty state if no content if (_homeData!.recentEpisodes.isEmpty && _homeData!.inProgressEpisodes.isEmpty && _homeData!.topPodcasts.isEmpty) _buildEmptyState(), ], ), ), ]), ); } Widget _buildStatsSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Your Library', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: _StatCard( title: 'Saved', count: _homeData!.savedCount, icon: Icons.bookmark, color: Colors.orange, ), ), const SizedBox(width: 12), Expanded( child: _StatCard( title: 'Downloaded', count: _homeData!.downloadedCount, icon: Icons.download, color: Colors.green, ), ), const SizedBox(width: 12), Expanded( child: _StatCard( title: 'Queue', count: _homeData!.queueCount, icon: Icons.queue_music, color: Colors.blue, ), ), ], ), ], ); } Widget _buildContinueListeningSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Continue Listening', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), ...(_homeData!.inProgressEpisodes.take(3).map((episode) => Padding( padding: const EdgeInsets.only(bottom: 12), child: _EpisodeCard( episode: episode, onTap: () { // Convert HomeEpisode to PinepodsEpisode for navigation final pinepodsEpisode = PinepodsEpisode( podcastName: episode.podcastName, episodeTitle: episode.episodeTitle, episodePubDate: episode.episodePubDate, episodeDescription: episode.episodeDescription ?? '', episodeArtwork: episode.episodeArtwork, episodeUrl: episode.episodeUrl, episodeDuration: episode.episodeDuration, listenDuration: episode.listenDuration, episodeId: episode.episodeId, completed: episode.completed, saved: episode.saved, queued: episode.queued, downloaded: episode.downloaded, isYoutube: episode.isYoutube, ); Navigator.push( context, MaterialPageRoute( builder: (context) => PinepodsEpisodeDetails( initialEpisode: pinepodsEpisode, ), ), ); }, onLongPress: () => _showContextMenu(_homeData!.inProgressEpisodes.indexOf(episode), true), onPlayPressed: () => _playEpisode(episode), ), ), )), ], ); } Widget _buildTopPodcastsSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Top Podcasts', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), SizedBox( height: 180, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _homeData!.topPodcasts.length, itemBuilder: (context, index) { final podcast = _homeData!.topPodcasts[index]; return Padding( padding: EdgeInsets.only( right: index < _homeData!.topPodcasts.length - 1 ? 16 : 0, ), child: _PodcastCard(podcast: podcast), ); }, ), ), ], ); } Widget _buildPlaylistsSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Smart Playlists', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), SizedBox( height: 120, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _playlistData!.playlists.length, itemBuilder: (context, index) { final playlist = _playlistData!.playlists[index]; return Padding( padding: EdgeInsets.only( right: index < _playlistData!.playlists.length - 1 ? 16 : 0, ), child: _PlaylistCard(playlist: playlist), ); }, ), ), ], ); } Widget _buildRecentEpisodesSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Recent Episodes', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), ...(_homeData!.recentEpisodes.take(5).map((episode) => Padding( padding: const EdgeInsets.only(bottom: 12), child: _EpisodeCard( episode: episode, onTap: () { // Convert HomeEpisode to PinepodsEpisode for navigation final pinepodsEpisode = PinepodsEpisode( podcastName: episode.podcastName, episodeTitle: episode.episodeTitle, episodePubDate: episode.episodePubDate, episodeDescription: episode.episodeDescription ?? '', episodeArtwork: episode.episodeArtwork, episodeUrl: episode.episodeUrl, episodeDuration: episode.episodeDuration, listenDuration: episode.listenDuration, episodeId: episode.episodeId, completed: episode.completed, saved: episode.saved, queued: episode.queued, downloaded: episode.downloaded, isYoutube: episode.isYoutube, ); Navigator.push( context, MaterialPageRoute( builder: (context) => PinepodsEpisodeDetails( initialEpisode: pinepodsEpisode, ), ), ); }, onLongPress: () => _showContextMenu(_homeData!.recentEpisodes.indexOf(episode), false), onPlayPressed: () => _playEpisode(episode), ), ), )), ], ); } Widget _buildEmptyState() { return Center( child: Padding( padding: const EdgeInsets.all(32.0), child: Column( children: [ Icon( Icons.podcasts_outlined, size: 64, color: Theme.of(context).colorScheme.primary.withOpacity(0.5), ), const SizedBox(height: 16), Text( 'Welcome to PinePods!', style: Theme.of(context).textTheme.headlineSmall, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'Start by searching for podcasts to subscribe to.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), textAlign: TextAlign.center, ), ], ), ), ); } void _navigateToPage(String pageName) { // This would be implemented to navigate to the appropriate page // For now, we'll show a placeholder snackbar ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Navigate to $pageName')), ); } } class _QuickLinkCard extends StatelessWidget { final String title; final IconData icon; final Color color; final VoidCallback onTap; const _QuickLinkCard({ required this.title, required this.icon, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return Card( child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: color, size: 32), const SizedBox(height: 8), Text( title, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), ], ), ), ), ); } } class _StatCard extends StatelessWidget { final String title; final int count; final IconData icon; final Color color; const _StatCard({ required this.title, required this.count, required this.icon, required this.color, }); @override Widget build(BuildContext context) { return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Icon(icon, color: color, size: 24), const SizedBox(height: 8), Text( count.toString(), style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: color, ), ), ], ), ), ); } } class _EpisodeCard extends StatelessWidget { final HomeEpisode episode; final VoidCallback? onTap; final VoidCallback? onLongPress; final VoidCallback? onPlayPressed; const _EpisodeCard({ required this.episode, this.onTap, this.onLongPress, this.onPlayPressed, }); @override Widget build(BuildContext context) { return Card( child: InkWell( onTap: onTap, onLongPress: onLongPress, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ // Episode artwork ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( episode.episodeArtwork, width: 60, height: 60, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( width: 60, height: 60, color: Theme.of(context).colorScheme.surfaceVariant, child: Icon( Icons.podcasts, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ); }, ), ), const SizedBox(width: 12), // Episode info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( episode.episodeTitle, style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( episode.podcastName, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), // Progress bar for in-progress episodes if (episode.listenDuration != null && episode.listenDuration! > 0) ...[ LinearProgressIndicator( value: episode.progressPercentage / 100, backgroundColor: Theme.of(context).colorScheme.surfaceVariant, valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( episode.formattedListenDuration ?? '', style: Theme.of(context).textTheme.bodySmall, ), Text( episode.formattedDuration, style: Theme.of(context).textTheme.bodySmall, ), ], ), ] else ...[ Text( episode.formattedDuration, style: Theme.of(context).textTheme.bodySmall, ), ], ], ), ), // Status indicators and play button Column( children: [ if (onPlayPressed != null) IconButton( onPressed: onPlayPressed, icon: Icon( episode.completed ? Icons.check_circle : ((episode.listenDuration != null && episode.listenDuration! > 0) ? Icons.play_circle_filled : Icons.play_circle_outline), color: episode.completed ? Colors.green : Theme.of(context).primaryColor, size: 28, ), padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 32, minHeight: 32, ), ), const SizedBox(height: 4), Row( mainAxisSize: MainAxisSize.min, children: [ if (episode.saved) Icon( Icons.bookmark, size: 16, color: Colors.orange[600], ), if (episode.downloaded) Padding( padding: const EdgeInsets.only(left: 4), child: Icon( Icons.download_done, size: 16, color: Colors.green[600], ), ), if (episode.queued) Padding( padding: const EdgeInsets.only(left: 4), child: Icon( Icons.queue_music, size: 16, color: Colors.blue[600], ), ), ], ), ], ), ], ), ), ), ); } } class _PodcastCard extends StatelessWidget { final HomePodcast podcast; const _PodcastCard({required this.podcast}); UnifiedPinepodsPodcast _convertToUnifiedPodcast() { return UnifiedPinepodsPodcast( id: podcast.podcastId, indexId: podcast.podcastIndexId ?? 0, title: podcast.podcastName, url: podcast.feedUrl ?? '', originalUrl: podcast.feedUrl ?? '', link: podcast.websiteUrl ?? '', description: podcast.description ?? '', author: podcast.author ?? '', ownerName: podcast.author ?? '', image: podcast.artworkUrl ?? '', artwork: podcast.artworkUrl ?? '', lastUpdateTime: 0, categories: podcast.categories != null ? {'0': podcast.categories!} : null, explicit: podcast.explicit ?? false, episodeCount: podcast.episodeCount ?? 0, ); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => PinepodsPodcastDetails( podcast: _convertToUnifiedPodcast(), isFollowing: true, ), ), ); }, child: SizedBox( width: 140, child: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network( podcast.artworkUrl ?? '', width: 140, height: 140, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( width: 140, height: 140, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceVariant, borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.podcasts, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ); }, ), ), const SizedBox(height: 8), Text( podcast.podcastName, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), ], ), ), ); } } class _PlaylistCard extends StatelessWidget { final Playlist playlist; const _PlaylistCard({required this.playlist}); @override Widget build(BuildContext context) { return SizedBox( width: 200, child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: InkWell( onTap: () => _openPlaylist(context), borderRadius: BorderRadius.circular(16), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Row( children: [ Icon( _getIconFromName(playlist.iconName), color: Theme.of(context).colorScheme.primary, size: 24, ), const SizedBox(width: 8), Expanded( child: Text( playlist.name, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), if (playlist.episodeCount != null) ...[ const SizedBox(height: 8), Text( '${playlist.episodeCount} episodes', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), ], ], ), ), ), ), ); } Future _openPlaylist(BuildContext context) async { final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; if (settings.pinepodsServer == null || settings.pinepodsApiKey == null || settings.pinepodsUserId == null) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Not connected to PinePods server. Please connect in Settings.'), backgroundColor: Colors.red, ), ); } return; } try { final pinepodsService = PinepodsService(); pinepodsService.setCredentials( settings.pinepodsServer!, settings.pinepodsApiKey!, ); final userPlaylists = await pinepodsService.getUserPlaylists(settings.pinepodsUserId!); final fullPlaylistData = userPlaylists.firstWhere( (p) => p.playlistId == playlist.playlistId, orElse: () => throw Exception('Playlist not found'), ); if (context.mounted) { Navigator.push( context, MaterialPageRoute( builder: (context) => PlaylistEpisodesPage(playlist: fullPlaylistData), ), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error opening playlist: $e'), backgroundColor: Colors.red, ), ); } } } IconData _getIconFromName(String iconName) { switch (iconName) { case 'ph-music-notes': return Icons.music_note; case 'ph-star': return Icons.star; case 'ph-clock': return Icons.access_time; case 'ph-heart': return Icons.favorite; default: return Icons.playlist_play; } } }