// lib/ui/pinepods/podcast_details.dart import 'package:flutter/material.dart'; import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart'; import 'package:pinepods_mobile/entities/pinepods_episode.dart'; import 'package:pinepods_mobile/entities/pinepods_search.dart'; import 'package:pinepods_mobile/entities/podcast.dart'; import 'package:pinepods_mobile/entities/episode.dart'; import 'package:pinepods_mobile/entities/person.dart'; import 'package:pinepods_mobile/services/pinepods/pinepods_service.dart'; import 'package:pinepods_mobile/services/podcast/podcast_service.dart'; import 'package:pinepods_mobile/services/global_services.dart'; import 'package:pinepods_mobile/ui/widgets/pinepods_episode_card.dart'; import 'package:pinepods_mobile/ui/widgets/platform_progress_indicator.dart'; import 'package:pinepods_mobile/ui/widgets/episode_context_menu.dart'; import 'package:pinepods_mobile/ui/widgets/podcast_image.dart'; import 'package:pinepods_mobile/ui/pinepods/episode_details.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/ui/podcast/mini_player.dart'; import 'package:pinepods_mobile/ui/utils/player_utils.dart'; import 'package:pinepods_mobile/ui/utils/local_download_utils.dart'; import 'package:provider/provider.dart'; import 'package:sliver_tools/sliver_tools.dart'; class PinepodsPodcastDetails extends StatefulWidget { final UnifiedPinepodsPodcast podcast; final bool isFollowing; final Function(bool)? onFollowChanged; const PinepodsPodcastDetails({ super.key, required this.podcast, required this.isFollowing, this.onFollowChanged, }); @override State createState() => _PinepodsPodcastDetailsState(); } class _PinepodsPodcastDetailsState extends State { final PinepodsService _pinepodsService = PinepodsService(); bool _isLoading = false; bool _isFollowing = false; bool _isFollowButtonLoading = false; String? _errorMessage; List _episodes = []; List _filteredEpisodes = []; int? _contextMenuEpisodeIndex; // Use global audio service instead of creating local instance final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; List _hosts = []; @override void initState() { super.initState(); _isFollowing = widget.isFollowing; _initializeCredentials(); _checkFollowStatus(); _searchController.addListener(_onSearchChanged); } @override void dispose() { _searchController.dispose(); // Don't dispose global audio service - it should persist across pages super.dispose(); } void _onSearchChanged() { setState(() { _searchQuery = _searchController.text; _filterEpisodes(); }); } void _filterEpisodes() { if (_searchQuery.isEmpty) { _filteredEpisodes = List.from(_episodes); } else { _filteredEpisodes = _episodes.where((episode) { return episode.episodeTitle.toLowerCase().contains(_searchQuery.toLowerCase()); }).toList(); } } void _initializeCredentials() { final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; if (settings.pinepodsServer != null && settings.pinepodsApiKey != null) { _pinepodsService.setCredentials( settings.pinepodsServer!, settings.pinepodsApiKey!, ); GlobalServices.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!); } } Future _checkFollowStatus() async { final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; final userId = settings.pinepodsUserId; if (userId == null) { setState(() { _isFollowing = false; }); _loadPodcastFeed(); return; } try { // If we have a valid podcast ID (> 0), assume it's followed since we got it from episode metadata if (widget.podcast.id > 0 && widget.isFollowing) { print('Using podcast ID ${widget.podcast.id} - assuming followed'); setState(() { _isFollowing = true; }); _loadPodcastFeed(); return; } print('Checking follow status for: ${widget.podcast.title}'); final isFollowing = await _pinepodsService.checkPodcastExists( widget.podcast.title, widget.podcast.url, userId, ); print('Follow status result: $isFollowing'); setState(() { _isFollowing = isFollowing; }); _loadPodcastFeed(); } catch (e) { print('Error checking follow status: $e'); // Use the passed value as fallback _loadPodcastFeed(); } } // Convert Episode objects to PinepodsEpisode objects PinepodsEpisode _convertEpisodeToPinepodsEpisode(Episode episode) { return PinepodsEpisode( podcastName: episode.podcast ?? widget.podcast.title, episodeTitle: episode.title ?? '', episodePubDate: episode.publicationDate?.toIso8601String() ?? '', episodeDescription: episode.description ?? '', episodeArtwork: episode.imageUrl ?? widget.podcast.artwork, episodeUrl: episode.contentUrl ?? '', episodeDuration: episode.duration, listenDuration: 0, // RSS episodes don't have listen duration episodeId: 0, // RSS episodes don't have server IDs completed: false, saved: false, queued: false, downloaded: false, isYoutube: false, ); } Future _loadPodcastFeed() async { setState(() { _isLoading = true; _errorMessage = null; }); try { final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; final userId = settings.pinepodsUserId; List episodes = []; if (_isFollowing && userId != null) { try { print('Loading episodes for followed podcast: ${widget.podcast.title}'); int? podcastId; // If we already have a podcast ID (from episode metadata), use it directly if (widget.podcast.id > 0) { podcastId = widget.podcast.id; print('Using existing podcast ID: $podcastId'); } else { // Get the actual podcast ID using the dedicated endpoint podcastId = await _pinepodsService.getPodcastId( userId, widget.podcast.url, widget.podcast.title, ); print('Got podcast ID from lookup: $podcastId'); } if (podcastId != null && podcastId > 0) { // Get episodes from server episodes = await _pinepodsService.getPodcastEpisodes(userId, podcastId); print('Loaded ${episodes.length} episodes from server for podcastId: $podcastId'); // If server has no episodes, this podcast may need episode sync if (episodes.isEmpty) { print('Server has no episodes for subscribed podcast. This should not happen.'); print('Podcast ID: $podcastId, Title: ${widget.podcast.title}'); // For subscribed podcasts, we should NOT fall back to RSS // The server should have episodes. This indicates a server-side sync issue. // Fall back to RSS ONLY as emergency backup, but episodes won't be clickable try { final podcastService = Provider.of(context, listen: false); final rssPodcast = Podcast.fromUrl(url: widget.podcast.url); final loadedPodcast = await podcastService.loadPodcast(podcast: rssPodcast); if (loadedPodcast != null && loadedPodcast.episodes.isNotEmpty) { episodes = loadedPodcast.episodes.map(_convertEpisodeToPinepodsEpisode).toList(); print('Emergency RSS fallback: Loaded ${episodes.length} episodes (NOT CLICKABLE)'); } } catch (e) { print('Emergency RSS fallback also failed: $e'); } } // Fetch podcast 2.0 data for hosts information try { final podcastData = await _pinepodsService.fetchPodcasting2PodData(podcastId, userId); if (podcastData != null) { final personsData = podcastData['people'] as List?; if (personsData != null) { final hosts = personsData.map((personData) { return Person( name: personData['name'] ?? '', role: personData['role'] ?? '', group: personData['group'] ?? '', image: personData['img'], link: personData['href'], ); }).toList(); setState(() { _hosts = hosts; }); print('Loaded ${hosts.length} hosts from podcast 2.0 data'); } } } catch (e) { print('Error loading podcast 2.0 data: $e'); } } else { print('No podcast ID found - podcast may not be properly added'); } } catch (e) { print('Error loading episodes for followed podcast: $e'); // Fall back to empty episodes list episodes = []; } } else { try { print('Loading episodes from RSS feed for non-followed podcast: ${widget.podcast.url}'); // Use the existing podcast service to parse RSS feed final podcastService = Provider.of(context, listen: false); final rssePodcast = Podcast.fromUrl(url: widget.podcast.url); final loadedPodcast = await podcastService.loadPodcast(podcast: rssePodcast); if (loadedPodcast != null && loadedPodcast.episodes.isNotEmpty) { // Convert Episode objects to PinepodsEpisode objects episodes = loadedPodcast.episodes.map(_convertEpisodeToPinepodsEpisode).toList(); print('Loaded ${episodes.length} episodes from RSS feed'); } else { print('No episodes found in RSS feed'); } } catch (e) { print('Error loading episodes from RSS feed: $e'); setState(() { _errorMessage = 'Failed to load podcast feed'; _isLoading = false; }); return; } } setState(() { _episodes = episodes; _filterEpisodes(); // Initialize filtered list _isLoading = false; }); } catch (e) { print('Error in _loadPodcastFeed: $e'); setState(() { _episodes = []; _isLoading = false; _errorMessage = 'Failed to load episodes'; }); } } Future _toggleFollow() async { print('PinePods Follow button: CLICKED - Setting loading to true'); setState(() { _isFollowButtonLoading = true; }); final settingsBloc = Provider.of(context, listen: false); final settings = settingsBloc.currentSettings; final userId = settings.pinepodsUserId; if (userId == null) { setState(() { _isFollowButtonLoading = false; }); _showSnackBar('Not logged in to PinePods server', Colors.red); return; } try { bool success; final oldFollowingState = _isFollowing; if (_isFollowing) { success = await _pinepodsService.removePodcast( widget.podcast.title, widget.podcast.url, userId, ); if (success) { setState(() { _isFollowing = false; }); widget.onFollowChanged?.call(false); _showSnackBar('Podcast removed', Colors.orange); } } else { success = await _pinepodsService.addPodcast(widget.podcast, userId); if (success) { setState(() { _isFollowing = true; }); widget.onFollowChanged?.call(true); _showSnackBar('Podcast added', Colors.green); } } if (success) { // Always reload episodes when follow status changes // This will switch between server episodes (followed) and RSS episodes (unfollowed) await _loadPodcastFeed(); } else { // Revert state change if the operation failed setState(() { _isFollowing = oldFollowingState; }); _showSnackBar('Failed to ${oldFollowingState ? 'remove' : 'add'} podcast', Colors.red); } } catch (e) { _showSnackBar('Error: $e', Colors.red); } finally { // Always reset loading state setState(() { _isFollowButtonLoading = false; }); print('PinePods Follow button: Loading state reset to false'); } } void _showSnackBar(String message, Color backgroundColor) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: backgroundColor, duration: const Duration(seconds: 2), ), ); } Future _showEpisodeContextMenu(int episodeIndex) async { final episode = _episodes[episodeIndex]; final isDownloadedLocally = await LocalDownloadUtils.isEpisodeDownloadedLocally(context, episode); if (!mounted) return; showDialog( context: context, barrierColor: Colors.black.withOpacity(0.3), builder: (context) => EpisodeContextMenu( episode: episode, isDownloadedLocally: isDownloadedLocally, onSave: () { Navigator.of(context).pop(); _saveEpisode(episodeIndex); }, onRemoveSaved: () { Navigator.of(context).pop(); _removeSavedEpisode(episodeIndex); }, onDownload: episode.downloaded ? () { Navigator.of(context).pop(); _deleteEpisode(episodeIndex); } : () { Navigator.of(context).pop(); _downloadEpisode(episodeIndex); }, onLocalDownload: () { Navigator.of(context).pop(); _localDownloadEpisode(episodeIndex); }, onDeleteLocalDownload: () { Navigator.of(context).pop(); _deleteLocalDownload(episodeIndex); }, onQueue: () { Navigator.of(context).pop(); _queueEpisode(episodeIndex); }, onMarkComplete: () { Navigator.of(context).pop(); _markEpisodeComplete(episodeIndex); }, onDismiss: () { Navigator.of(context).pop(); _hideEpisodeContextMenu(); }, ), ); } void _hideEpisodeContextMenu() { setState(() { _contextMenuEpisodeIndex = null; }); } PinepodsAudioService? get _audioService => GlobalServices.pinepodsAudioService; Future _playEpisode(PinepodsEpisode episode) async { if (_audioService == null) { _showSnackBar('Audio service not available', Colors.red); return; } try { await playPinepodsEpisodeWithOptionalFullScreen( context, _audioService!, episode, resume: episode.isStarted, ); } catch (e) { _showSnackBar('Failed to play episode: $e', Colors.red); } } Future _saveEpisode(int episodeIndex) async { final episode = _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!); try { final success = await _pinepodsService.saveEpisode( episode.episodeId, userId, episode.isYoutube, ); if (success) { setState(() { _episodes[episodeIndex] = _updateEpisodeProperty(episode, saved: true); _filteredEpisodes = _episodes.where((e) => e.episodeTitle.toLowerCase().contains(_searchController.text.toLowerCase()) ).toList(); }); _showSnackBar('Episode saved!', Colors.green); } else { _showSnackBar('Failed to save episode', Colors.red); } } catch (e) { _showSnackBar('Error saving episode: $e', Colors.red); } _hideEpisodeContextMenu(); } Future _removeSavedEpisode(int episodeIndex) async { final episode = _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!); try { final success = await _pinepodsService.removeSavedEpisode( episode.episodeId, userId, episode.isYoutube, ); if (success) { setState(() { _episodes[episodeIndex] = _updateEpisodeProperty(episode, saved: false); _filteredEpisodes = _episodes.where((e) => e.episodeTitle.toLowerCase().contains(_searchController.text.toLowerCase()) ).toList(); }); _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); } _hideEpisodeContextMenu(); } Future _downloadEpisode(int episodeIndex) async { final episode = _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!); try { final success = await _pinepodsService.downloadEpisode( episode.episodeId, userId, episode.isYoutube, ); if (success) { setState(() { _episodes[episodeIndex] = _updateEpisodeProperty(episode, downloaded: true); _filteredEpisodes = _episodes.where((e) => e.episodeTitle.toLowerCase().contains(_searchController.text.toLowerCase()) ).toList(); }); _showSnackBar('Episode download started!', Colors.green); } else { _showSnackBar('Failed to download episode', Colors.red); } } catch (e) { _showSnackBar('Error downloading episode: $e', Colors.red); } _hideEpisodeContextMenu(); } Future _deleteEpisode(int episodeIndex) async { final episode = _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!); try { final success = await _pinepodsService.deleteEpisode( episode.episodeId, userId, episode.isYoutube, ); if (success) { setState(() { _episodes[episodeIndex] = _updateEpisodeProperty(episode, downloaded: false); _filteredEpisodes = _episodes.where((e) => e.episodeTitle.toLowerCase().contains(_searchController.text.toLowerCase()) ).toList(); }); _showSnackBar('Episode deleted from server', Colors.orange); } else { _showSnackBar('Failed to delete episode', Colors.red); } } catch (e) { _showSnackBar('Error deleting episode: $e', Colors.red); } _hideEpisodeContextMenu(); } Future _queueEpisode(int episodeIndex) async { final episode = _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!); try { bool success; if (episode.queued) { success = await _pinepodsService.removeQueuedEpisode( episode.episodeId, userId, episode.isYoutube, ); if (success) { setState(() { _episodes[episodeIndex] = _updateEpisodeProperty(episode, queued: false); _filteredEpisodes = _episodes.where((e) => e.episodeTitle.toLowerCase().contains(_searchController.text.toLowerCase()) ).toList(); }); _showSnackBar('Removed from queue', Colors.orange); } } else { success = await _pinepodsService.queueEpisode( episode.episodeId, userId, episode.isYoutube, ); if (success) { setState(() { _episodes[episodeIndex] = _updateEpisodeProperty(episode, queued: true); _filteredEpisodes = _episodes.where((e) => e.episodeTitle.toLowerCase().contains(_searchController.text.toLowerCase()) ).toList(); }); _showSnackBar('Added to queue!', Colors.green); } } if (!success) { _showSnackBar('Failed to update queue', Colors.red); } } catch (e) { _showSnackBar('Error updating queue: $e', Colors.red); } _hideEpisodeContextMenu(); } Future _markEpisodeComplete(int episodeIndex) async { final episode = _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!); try { final success = await _pinepodsService.markEpisodeCompleted( episode.episodeId, userId, episode.isYoutube, ); if (success) { setState(() { _episodes[episodeIndex] = _updateEpisodeProperty(episode, completed: true); _filteredEpisodes = _episodes.where((e) => e.episodeTitle.toLowerCase().contains(_searchController.text.toLowerCase()) ).toList(); }); _showSnackBar('Episode marked as complete', Colors.green); } else { _showSnackBar('Failed to mark episode complete', Colors.red); } } catch (e) { _showSnackBar('Error marking episode complete: $e', Colors.red); } _hideEpisodeContextMenu(); } Future _localDownloadEpisode(int episodeIndex) async { final episode = _episodes[episodeIndex]; final success = await LocalDownloadUtils.localDownloadEpisode(context, episode); if (success) { _showSnackBar('Episode download started', Colors.green); } else { _showSnackBar('Failed to start download', Colors.red); } _hideEpisodeContextMenu(); } Future _deleteLocalDownload(int episodeIndex) async { final episode = _episodes[episodeIndex]; final deletedCount = await LocalDownloadUtils.deleteLocalDownload(context, episode); if (deletedCount > 0) { _showSnackBar( 'Deleted $deletedCount local download${deletedCount > 1 ? 's' : ''}', Colors.orange ); } else { _showSnackBar('Local download not found', Colors.red); } _hideEpisodeContextMenu(); } PinepodsEpisode _updateEpisodeProperty( PinepodsEpisode episode, { bool? saved, bool? downloaded, bool? queued, bool? completed, }) { return 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: completed ?? episode.completed, saved: saved ?? episode.saved, queued: queued ?? episode.queued, downloaded: downloaded ?? episode.downloaded, isYoutube: episode.isYoutube, podcastId: episode.podcastId, ); } @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ Expanded( child: CustomScrollView( slivers: [ SliverAppBar( expandedHeight: 300, pinned: true, flexibleSpace: FlexibleSpaceBar( title: Text( widget.podcast.title, style: const TextStyle( shadows: [ Shadow( offset: Offset(0, 1), blurRadius: 3, color: Colors.black54, ), ], ), ), background: Stack( fit: StackFit.expand, children: [ widget.podcast.artwork.isNotEmpty ? Image.network( widget.podcast.artwork, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( color: Colors.grey[300], child: const Icon( Icons.music_note, size: 80, color: Colors.grey, ), ); }, ) : Container( color: Colors.grey[300], child: const Icon( Icons.music_note, size: 80, color: Colors.grey, ), ), Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withOpacity(0.7), ], ), ), ), ], ), ), actions: [ IconButton( onPressed: _isFollowButtonLoading ? null : _toggleFollow, icon: _isFollowButtonLoading ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2.0, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Icon( _isFollowing ? Icons.favorite : Icons.favorite_border, color: _isFollowing ? Colors.red : Colors.white, ), tooltip: _isFollowing ? 'Unfollow' : 'Follow', ), ], ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Podcast info with follow/unfollow button Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.podcast.author.isNotEmpty) Text( 'By ${widget.podcast.author}', style: TextStyle( fontSize: 16, color: Theme.of(context).primaryColor, fontWeight: FontWeight.w500, ), ), ], ), ), ElevatedButton.icon( onPressed: _isFollowButtonLoading ? null : _toggleFollow, icon: _isFollowButtonLoading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2.0, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Icon( _isFollowing ? Icons.remove : Icons.add, size: 16, ), label: Text(_isFollowing ? 'Unfollow' : 'Follow'), style: ElevatedButton.styleFrom( backgroundColor: _isFollowing ? Colors.red : Colors.green, foregroundColor: Colors.white, ), ), ], ), const SizedBox(height: 8), Text( widget.podcast.description, style: const TextStyle(fontSize: 14), ), const SizedBox(height: 16), // Podcast stats Row( children: [ Icon( Icons.mic, size: 16, color: Colors.grey[600], ), const SizedBox(width: 4), Text( '${widget.podcast.episodeCount} episode${widget.podcast.episodeCount != 1 ? 's' : ''}', style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), const SizedBox(width: 16), if (widget.podcast.explicit) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(4), ), child: const Text( 'Explicit', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ), // Hosts section (filter out "Unknown Host" entries) if (_hosts.where((host) => host.name != "Unknown Host").isNotEmpty) ...[ const SizedBox(height: 16), Text( 'Hosts', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey[800], ), ), const SizedBox(height: 8), SizedBox( height: 80, child: Builder(builder: (context) { final actualHosts = _hosts.where((host) => host.name != "Unknown Host").toList(); return ListView.builder( scrollDirection: Axis.horizontal, itemCount: actualHosts.length, itemBuilder: (context, index) { final host = actualHosts[index]; return Container( width: 70, margin: const EdgeInsets.only(right: 12), child: Column( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.grey[300], ), child: host.image != null && host.image!.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(25), child: PodcastImage( url: host.image!, width: 50, height: 50, fit: BoxFit.cover, ), ) : const Icon( Icons.person, size: 30, color: Colors.grey, ), ), const SizedBox(height: 4), Text( host.name, style: const TextStyle(fontSize: 12), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); }, ); }), ), ], const SizedBox(height: 24), // Episodes section header Row( children: [ const Text( 'Episodes', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), const Spacer(), if (_isLoading) const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ], ), const SizedBox(height: 16), ], ), ), ), // Episodes list if (_isLoading) const SliverToBoxAdapter( child: Center( child: Padding( padding: EdgeInsets.all(32.0), child: PlatformProgressIndicator(), ), ), ) else if (_errorMessage != null) SliverToBoxAdapter( child: Center( child: Padding( padding: const EdgeInsets.all(32.0), child: Column( children: [ Icon( Icons.error_outline, size: 64, color: Colors.red[300], ), const SizedBox(height: 16), Text( _errorMessage!, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 16), ElevatedButton( onPressed: _loadPodcastFeed, child: const Text('Retry'), ), ], ), ), ), ) else if (_episodes.isEmpty) SliverToBoxAdapter( child: Center( child: Padding( padding: const EdgeInsets.all(32.0), child: Column( children: [ Icon( Icons.info_outline, size: 64, color: Colors.blue[300], ), const SizedBox(height: 16), Text( _isFollowing ? 'No episodes found' : 'Episodes available after following', style: Theme.of(context).textTheme.headlineSmall, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( _isFollowing ? 'Episodes from your PinePods library will appear here' : 'Follow this podcast to add it to your library and view episodes', style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton( onPressed: _toggleFollow, child: Text(_isFollowing ? 'Unfollow' : 'Follow'), ), ], ), ), ), ) else MultiSliver( children: [ _buildSearchBar(), _buildEpisodesList(), ], ), ], ), ), const MiniPlayer(), ], ), ); } Widget _buildSearchBar() { return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16.0), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Filter episodes...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), filled: true, fillColor: Theme.of(context).cardColor, ), ), ), ); } Widget _buildEpisodesList() { // Check if search returned no results if (_filteredEpisodes.isEmpty && _searchQuery.isNotEmpty) { return SliverFillRemaining( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.search_off, size: 64, color: Theme.of(context).primaryColor, ), const SizedBox(height: 16), Text( 'No episodes found', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'No episodes match "$_searchQuery"', style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), ], ), ), ); } return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { final episode = _filteredEpisodes[index]; // Find the original index for context menu operations final originalIndex = _episodes.indexOf(episode); final bool hasValidServerEpisodeId = episode.episodeId > 0; if (!hasValidServerEpisodeId) { print('Episode "${episode.episodeTitle}" has no server ID (RSS fallback) - disabling episode details navigation'); } return PinepodsEpisodeCard( episode: episode, onTap: _isFollowing && hasValidServerEpisodeId ? () { // Navigate to episode details only if following AND has valid server episode ID Navigator.push( context, MaterialPageRoute( builder: (context) => PinepodsEpisodeDetails( initialEpisode: episode, ), ), ); } : null, // Disable tap if not following or no valid episode ID onLongPress: _isFollowing && hasValidServerEpisodeId ? () { _showEpisodeContextMenu(originalIndex); } : null, // Disable long press if not following or no valid episode ID onPlayPressed: _isFollowing ? () { _playEpisode(episode); } : null, // Allow play for RSS episodes since it uses direct URL ); }, childCount: _filteredEpisodes.length, ), ); } }