diff --git a/mobile/lib/ui/pinepods/episode_details.dart b/mobile/lib/ui/pinepods/episode_details.dart index 0fb271a5..a8761873 100644 --- a/mobile/lib/ui/pinepods/episode_details.dart +++ b/mobile/lib/ui/pinepods/episode_details.dart @@ -624,333 +624,335 @@ class _PinepodsEpisodeDetailsState extends State { title: Text(_episode!.podcastName), elevation: 0, ), - body: Column( - children: [ - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Episode artwork and basic info + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Episode artwork and basic info - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Episode artwork - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: _episode!.episodeArtwork.isNotEmpty - ? Image.network( - _episode!.episodeArtwork, - width: 120, - height: 120, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - width: 120, - height: 120, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.music_note, - color: Colors.grey, - size: 48, - ), - ); - }, - ) - : Container( - width: 120, - height: 120, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(8), + // Episode artwork + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: _episode!.episodeArtwork.isNotEmpty + ? Image.network( + _episode!.episodeArtwork, + width: 120, + height: 120, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.music_note, + color: Colors.grey, + size: 48, + ), + ); + }, + ) + : Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.music_note, + color: Colors.grey, + size: 48, + ), ), - child: const Icon( - Icons.music_note, - color: Colors.grey, - size: 48, + ), + const SizedBox(width: 16), + // Episode info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Clickable podcast name + GestureDetector( + onTap: () => _navigateToPodcast(), + child: Text( + _episode!.podcastName, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.w500, + decoration: TextDecoration.underline, + decorationColor: Theme.of(context).primaryColor, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), - ), - const SizedBox(width: 16), - // Episode info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Clickable podcast name - GestureDetector( - onTap: () => _navigateToPodcast(), - child: Text( - _episode!.podcastName, - style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - decoration: TextDecoration.underline, - decorationColor: Theme.of(context).primaryColor, + const SizedBox(height: 4), + Text( + _episode!.episodeTitle, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + fontWeight: FontWeight.bold, ), - maxLines: 2, + maxLines: 3, overflow: TextOverflow.ellipsis, ), - ), - const SizedBox(height: 4), - Text( - _episode!.episodeTitle, - style: Theme.of(context).textTheme.titleLarge!.copyWith( - fontWeight: FontWeight.bold, - ), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 8), - Text( - _episode!.formattedDuration, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Colors.grey[600], - ), - ), - const SizedBox(height: 4), - Text( - _episode!.formattedPubDate, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Colors.grey[600], - ), - ), - if (_episode!.isStarted) ...[ const SizedBox(height: 8), Text( - 'Listened: ${_episode!.formattedListenDuration}', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: Theme.of(context).primaryColor, + _episode!.formattedDuration, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Colors.grey[600], ), ), const SizedBox(height: 4), - LinearProgressIndicator( - value: _episode!.progressPercentage / 100, - backgroundColor: Colors.grey[300], - valueColor: AlwaysStoppedAnimation( - Theme.of(context).primaryColor, + Text( + _episode!.formattedPubDate, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Colors.grey[600], ), ), + if (_episode!.isStarted) ...[ + const SizedBox(height: 8), + Text( + 'Listened: ${_episode!.formattedListenDuration}', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(height: 4), + LinearProgressIndicator( + value: _episode!.progressPercentage / 100, + backgroundColor: Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + Theme.of(context).primaryColor, + ), + ), + ], ], - ], + ), ), - ), - ], - ), + ], + ), - const SizedBox(height: 24), + const SizedBox(height: 24), - // Action buttons - Column( - children: [ - // First row: Play, Save, Queue (3 buttons, each 1/3 width) - Row( - children: [ - // Play/Pause button - Expanded( - child: StreamBuilder( - stream: Provider.of(context, listen: false).playingState, - builder: (context, snapshot) { - final isCurrentEpisode = _isCurrentEpisodePlaying(); - final isPlaying = snapshot.data == AudioState.playing; - final isCurrentlyPlaying = isCurrentEpisode && isPlaying; + // Action buttons + Column( + children: [ + // First row: Play, Save, Queue (3 buttons, each 1/3 width) + Row( + children: [ + // Play/Pause button + Expanded( + child: StreamBuilder( + stream: Provider.of(context, listen: false).playingState, + builder: (context, snapshot) { + final isCurrentEpisode = _isCurrentEpisodePlaying(); + final isPlaying = snapshot.data == AudioState.playing; + final isCurrentlyPlaying = isCurrentEpisode && isPlaying; - IconData icon; - String label; + IconData icon; + String label; - if (_episode!.completed) { - icon = Icons.replay; - label = 'Replay'; - } else if (isCurrentlyPlaying) { - icon = Icons.pause; - label = 'Pause'; - } else { - icon = Icons.play_arrow; - label = 'Play'; - } + if (_episode!.completed) { + icon = Icons.replay; + label = 'Replay'; + } else if (isCurrentlyPlaying) { + icon = Icons.pause; + label = 'Pause'; + } else { + icon = Icons.play_arrow; + label = 'Play'; + } - return OutlinedButton.icon( - onPressed: _togglePlayPause, - icon: Icon(icon), - label: Text(label), - ); - }, + return OutlinedButton.icon( + onPressed: _togglePlayPause, + icon: Icon(icon), + label: Text(label), + ); + }, + ), ), - ), - const SizedBox(width: 8), + const SizedBox(width: 8), - // Save/Unsave button - Expanded( - child: OutlinedButton.icon( - onPressed: _episode!.saved ? _removeSavedEpisode : _saveEpisode, - icon: Icon( - _episode!.saved ? Icons.bookmark : Icons.bookmark_outline, - color: _episode!.saved ? Colors.orange : null, + // Save/Unsave button + Expanded( + child: OutlinedButton.icon( + onPressed: _episode!.saved ? _removeSavedEpisode : _saveEpisode, + icon: Icon( + _episode!.saved ? Icons.bookmark : Icons.bookmark_outline, + color: _episode!.saved ? Colors.orange : null, + ), + label: Text(_episode!.saved ? 'Saved' : 'Save'), ), - label: Text(_episode!.saved ? 'Saved' : 'Save'), ), - ), - const SizedBox(width: 8), + const SizedBox(width: 8), - // Queue button - Expanded( - child: OutlinedButton.icon( - onPressed: _toggleQueue, - icon: Icon( - _episode!.queued ? Icons.queue_music : Icons.queue_music_outlined, - color: _episode!.queued ? Colors.purple : null, + // Queue button + Expanded( + child: OutlinedButton.icon( + onPressed: _toggleQueue, + icon: Icon( + _episode!.queued ? Icons.queue_music : Icons.queue_music_outlined, + color: _episode!.queued ? Colors.purple : null, + ), + label: Text(_episode!.queued ? 'Queued' : 'Queue'), ), - label: Text(_episode!.queued ? 'Queued' : 'Queue'), ), - ), - ], - ), + ], + ), - const SizedBox(height: 8), + const SizedBox(height: 8), - // Second row: Download, Complete (2 buttons, each 1/2 width) - Row( - children: [ - // Download button - Expanded( - child: OutlinedButton.icon( - onPressed: _toggleDownload, - icon: Icon( - _episode!.downloaded ? Icons.download_done : Icons.download_outlined, - color: _episode!.downloaded ? Colors.blue : null, + // Second row: Download, Complete (2 buttons, each 1/2 width) + Row( + children: [ + // Download button + Expanded( + child: OutlinedButton.icon( + onPressed: _toggleDownload, + icon: Icon( + _episode!.downloaded ? Icons.download_done : Icons.download_outlined, + color: _episode!.downloaded ? Colors.blue : null, + ), + label: Text(_episode!.downloaded ? 'Downloaded' : 'Download'), ), - label: Text(_episode!.downloaded ? 'Downloaded' : 'Download'), ), - ), - const SizedBox(width: 8), + const SizedBox(width: 8), - // Complete button - Expanded( - child: OutlinedButton.icon( - onPressed: _toggleComplete, - icon: Icon( - _episode!.completed ? Icons.check_circle : Icons.check_circle_outline, - color: _episode!.completed ? Colors.green : null, + // Complete button + Expanded( + child: OutlinedButton.icon( + onPressed: _toggleComplete, + icon: Icon( + _episode!.completed ? Icons.check_circle : Icons.check_circle_outline, + color: _episode!.completed ? Colors.green : null, + ), + label: Text(_episode!.completed ? 'Complete' : 'Mark Complete'), ), - label: Text(_episode!.completed ? 'Complete' : 'Mark Complete'), ), - ), - ], - ), + ], + ), - const SizedBox(height: 8), + const SizedBox(height: 8), - // Third row: Local Download (full width) - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: _isDownloadedLocally ? _deleteLocalDownload : _localDownloadEpisode, - icon: Icon( - _isDownloadedLocally ? Icons.delete_forever_outlined : Icons.file_download_outlined, - color: _isDownloadedLocally ? Colors.red : Colors.green, - ), - label: Text(_isDownloadedLocally ? 'Delete Local Download' : 'Download Locally'), - style: OutlinedButton.styleFrom( - side: BorderSide( + // Third row: Local Download (full width) + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _isDownloadedLocally ? _deleteLocalDownload : _localDownloadEpisode, + icon: Icon( + _isDownloadedLocally ? Icons.delete_forever_outlined : Icons.file_download_outlined, color: _isDownloadedLocally ? Colors.red : Colors.green, ), + label: Text(_isDownloadedLocally ? 'Delete Local Download' : 'Download Locally'), + style: OutlinedButton.styleFrom( + side: BorderSide( + color: _isDownloadedLocally ? Colors.red : Colors.green, + ), + ), ), ), - ), - ], - ), - ], - ), + ], + ), + ], + ), - // Hosts/Guests section - if (_persons.isNotEmpty) ...[ - const SizedBox(height: 24), - Align( - alignment: Alignment.centerLeft, - child: Text( - 'Hosts & Guests', - style: Theme.of(context).textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.bold, + // Hosts/Guests section + if (_persons.isNotEmpty) ...[ + const SizedBox(height: 24), + Align( + alignment: Alignment.centerLeft, + child: Text( + 'Hosts & Guests', + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + ), ), ), - ), - const SizedBox(height: 12), - SizedBox( - height: 80, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: _persons.length, - itemBuilder: (context, index) { - final person = _persons[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: person.image != null && person.image!.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(25), - child: PodcastImage( - url: person.image!, - width: 50, - height: 50, - fit: BoxFit.cover, + const SizedBox(height: 12), + SizedBox( + height: 80, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _persons.length, + itemBuilder: (context, index) { + final person = _persons[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: person.image != null && person.image!.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(25), + child: PodcastImage( + url: person.image!, + width: 50, + height: 50, + fit: BoxFit.cover, + ), + ) + : const Icon( + Icons.person, + size: 30, + color: Colors.grey, ), - ) - : const Icon( - Icons.person, - size: 30, - color: Colors.grey, - ), - ), - const SizedBox(height: 4), - Text( - person.name, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ); - }, + ), + const SizedBox(height: 4), + Text( + person.name, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + }, + ), ), - ), - ], + ], - const SizedBox(height: 32), + const SizedBox(height: 32), - // Episode description - Text( - 'Description', - style: Theme.of(context).textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.bold, + // Episode description + Text( + 'Description', + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox(height: 12), - EpisodeDescription( - content: _episode!.episodeDescription, - onTimestampTap: _handleTimestampTap, - ), - ], + const SizedBox(height: 12), + EpisodeDescription( + content: _episode!.episodeDescription, + onTimestampTap: _handleTimestampTap, + ), + ], + ), ), ), - ), - const MiniPlayer(), - ], + const MiniPlayer(), + ], + ), ), ); } @@ -960,4 +962,4 @@ class _PinepodsEpisodeDetailsState extends State { // Don't dispose global audio service - it should persist across pages super.dispose(); } -} \ No newline at end of file +} diff --git a/mobile/lib/ui/pinepods/podcast_details.dart b/mobile/lib/ui/pinepods/podcast_details.dart index bbc908cd..8f124ac7 100644 --- a/mobile/lib/ui/pinepods/podcast_details.dart +++ b/mobile/lib/ui/pinepods/podcast_details.dart @@ -764,366 +764,368 @@ class _PinepodsPodcastDetailsState extends State { @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, - ), - ], + body: SafeArea( + child: 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, + 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), + ], ), - 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), + 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, ), - ) - : Icon( - _isFollowing ? Icons.favorite : Icons.favorite_border, - color: _isFollowing ? Colors.red : Colors.white, - ), - tooltip: _isFollowing ? 'Unfollow' : 'Follow', - ), - ], - ), + 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, + 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), + 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, ), - ) - : 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, + label: Text(_isFollowing ? 'Unfollow' : 'Follow'), + style: ElevatedButton.styleFrom( + backgroundColor: _isFollowing ? Colors.red : Colors.green, + foregroundColor: Colors.white, + ), ), - ), - ], - ), + ], + ), - const SizedBox(height: 8), + const SizedBox(height: 8), - Text( - widget.podcast.description, - style: const TextStyle(fontSize: 14), - ), + Text( + widget.podcast.description, + style: const TextStyle(fontSize: 14), + ), - const SizedBox(height: 16), + 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, + // Podcast stats + Row( + children: [ + Icon( + Icons.mic, + size: 16, 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, - ), + const SizedBox(width: 4), + Text( + '${widget.podcast.episodeCount} episode${widget.podcast.episodeCount != 1 ? 's' : ''}', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], ), ), - ], - ), - - // 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(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, + ), + ), + ), + ], ), - ], - const SizedBox(height: 24), - - // Episodes section header - Row( - children: [ - const Text( - 'Episodes', + // 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: 20, + fontSize: 16, fontWeight: FontWeight.bold, + color: Colors.grey[800], ), ), - const Spacer(), - if (_isLoading) - const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ), + 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: 16), - ], + 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(), + // 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 (_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 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(), + ], + ), + ], ), - ) - else - MultiSliver( - children: [ - _buildSearchBar(), - _buildEpisodesList(), - ], - ), - ], ), - ), - const MiniPlayer(), - ], + const MiniPlayer(), + ], + ), ), ); } @@ -1224,4 +1226,4 @@ class _PinepodsPodcastDetailsState extends State { ), ); } -} \ No newline at end of file +}