Merge branch 'feature/podcast-cover-art' into 'develop'

feat(podcasts): show cover art on podcast channel list

See merge request ultrasonic/ultrasonic!1442
This commit is contained in:
birdbird committed 2026-10-17 12:05:34 +00:00
commit 1ec3a1b49d
5 files changed
+64 -11

No files matched your search

@@ -7,7 +7,8 @@ data class PodcastsChannel(
val title: String?,
val url: String?,
val description: String?,
val status: String?
val status: String?,
val coverArt: String? = null
) : GenericEntry(),
Serializable {
companion object {
@@ -0,0 +1,50 @@
/*
* PodcastChannelRowBinder.kt
* Copyright (C) 2009-2026 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isGone
import com.drakeet.multitype.ItemViewBinder
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.PodcastsChannel
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.FileUtil
class PodcastChannelRowBinder(
private val onItemClick: (PodcastsChannel) -> Unit,
private val imageLoaderProvider: ImageLoaderProvider
) : ItemViewBinder<PodcastsChannel, AlbumRowDelegate.ListViewHolder>() {
override fun onBindViewHolder(holder: AlbumRowDelegate.ListViewHolder, item: PodcastsChannel) {
holder.album.text = item.title
holder.artist.text = item.url
holder.star.isGone = true
holder.details.setOnClickListener { onItemClick(item) }
holder.coverArt.setOnClickListener { onItemClick(item) }
if (item.coverArt != null) {
imageLoaderProvider.executeOn {
it.loadImage(
holder.coverArt,
item.coverArt,
FileUtil.getArtistArtKey(item.title, false),
false,
0
)
}
} else {
holder.coverArt.setImageResource(R.drawable.unknown_album)
}
}
override fun onCreateViewHolder(
inflater: LayoutInflater,
parent: ViewGroup
): AlbumRowDelegate.ListViewHolder = AlbumRowDelegate.ListViewHolder(parent, inflater)
}
@@ -11,7 +11,8 @@ fun PodcastChannel.toDomainEntity(): PodcastsChannel = PodcastsChannel(
this.title,
this.url,
this.description,
this.status
this.status,
this.coverArt.ifEmpty { null }
)
fun List<PodcastChannel>.toDomainEntitiesList(): List<PodcastsChannel> = this
@@ -12,12 +12,10 @@ import android.view.MenuItem
import android.view.View
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.PopupMenuFactory
import org.moire.ultrasonic.adapters.TextRowBinder
import org.moire.ultrasonic.adapters.PodcastChannelRowBinder
import org.moire.ultrasonic.domain.PodcastsChannel
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
import org.moire.ultrasonic.model.PodcastListModel
@@ -27,8 +25,6 @@ import org.moire.ultrasonic.model.PodcastListModel
*/
class PodcastFragment : MultiListFragment<PodcastsChannel>() {
private val popupMenuFactory: PopupMenuFactory by inject()
/**
* The ViewModel to use to get the data
*/
@@ -53,11 +49,9 @@ class PodcastFragment : MultiListFragment<PodcastsChannel>() {
viewAdapter.register(
PodcastsChannel::class.java,
TextRowBinder(
textResolver = { it.title },
PodcastChannelRowBinder(
onItemClick = { onItemClick(it) },
onContextMenuItemClick = { _, _ -> false },
popupMenuFactory = popupMenuFactory
imageLoaderProvider = imageLoaderProvider
)
)
}
@@ -31,9 +31,16 @@ class APIPodcastConverterTest {
status `should be equal to` entity.status
title `should be equal to` entity.title
url `should be equal to` entity.url
coverArt `should be equal to` entity.coverArt
}
}
@Test
fun `Should map empty coverArt to null`() {
val entity = PodcastChannel(id = "1", coverArt = "")
entity.toDomainEntity().coverArt `should be equal to` null
}
@Test
fun `Should convert list of podcasts channels to domain entites list`() {
val entitiesList = listOf(