0
0
mirror of https://github.com/mediathekview/zapp.git synced 2024-09-20 04:12:14 +02:00

Use same ViewHolder for all mediathek items

This commit is contained in:
Christine Coenen 2022-11-16 17:37:15 +01:00
parent 216c6137fe
commit 49e4cfbc1e
9 changed files with 122 additions and 181 deletions

View File

@ -5,7 +5,9 @@ import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.paging.PagingDataAdapter
import de.christinecoenen.code.zapp.databinding.DownloadsFragmentListItemBinding
import de.christinecoenen.code.zapp.app.mediathek.ui.list.adapter.MediathekItemType
import de.christinecoenen.code.zapp.app.mediathek.ui.list.adapter.MediathekItemViewHolder
import de.christinecoenen.code.zapp.databinding.MediathekListFragmentItemBinding
import de.christinecoenen.code.zapp.models.shows.PersistedMediathekShow
import de.christinecoenen.code.zapp.utils.view.PersistedMediathekShowDiffUtilItemCallback
import kotlinx.coroutines.Dispatchers
@ -15,12 +17,14 @@ import kotlinx.coroutines.launch
class DownloadListAdapter(
private val scope: LifecycleCoroutineScope,
private val listener: Listener
) : PagingDataAdapter<PersistedMediathekShow, DownloadViewHolder>(PersistedMediathekShowDiffUtilItemCallback()) {
) : PagingDataAdapter<PersistedMediathekShow, MediathekItemViewHolder>(
PersistedMediathekShowDiffUtilItemCallback()
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediathekItemViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = DownloadsFragmentListItemBinding.inflate(layoutInflater, parent, false)
val holder = DownloadViewHolder(binding)
val binding = MediathekListFragmentItemBinding.inflate(layoutInflater, parent, false)
val holder = MediathekItemViewHolder(binding, MediathekItemType.Download)
binding.root.setOnClickListener {
getItem(holder.bindingAdapterPosition)?.let {
@ -37,10 +41,10 @@ class DownloadListAdapter(
return holder
}
override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) {
override fun onBindViewHolder(holder: MediathekItemViewHolder, position: Int) {
getItem(position)?.let {
scope.launch(Dispatchers.Main) {
holder.bindItem(it)
holder.setShow(it.mediathekShow)
}
}
}

View File

@ -1,152 +0,0 @@
package de.christinecoenen.code.zapp.app.downloads.ui.list.adapter
import android.graphics.Bitmap
import android.view.View
import android.widget.ImageView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import de.christinecoenen.code.zapp.R
import de.christinecoenen.code.zapp.databinding.DownloadsFragmentListItemBinding
import de.christinecoenen.code.zapp.models.shows.DownloadStatus
import de.christinecoenen.code.zapp.models.shows.PersistedMediathekShow
import de.christinecoenen.code.zapp.repositories.MediathekRepository
import de.christinecoenen.code.zapp.utils.system.ImageHelper
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class DownloadViewHolder(
val binding: DownloadsFragmentListItemBinding
) : RecyclerView.ViewHolder(binding.root), KoinComponent {
private val mediathekRepository: MediathekRepository by inject()
private var downloadProgressJob: Job? = null
private var downloadStatusJob: Job? = null
private var playbackPositionJob: Job? = null
private var videoPathJob: Job? = null
private var showt: PersistedMediathekShow? = null
suspend fun bindItem(show: PersistedMediathekShow) = withContext(Dispatchers.Main) {
binding.root.visibility = View.GONE
showt = show
downloadProgressJob?.cancel()
downloadStatusJob?.cancel()
playbackPositionJob?.cancel()
videoPathJob?.cancel()
binding.topic.text = show.mediathekShow.topic
binding.title.text = show.mediathekShow.title
binding.duration.text = show.mediathekShow.formattedDuration
binding.channel.text = show.mediathekShow.channel
binding.time.text = show.mediathekShow.formattedTimestamp
binding.downloadProgress.isVisible = false
binding.downloadProgress.progress = 0
binding.icon.setImageDrawable(null)
updateThumbnail(null)
binding.root.visibility = View.VISIBLE
downloadProgressJob = launch { updateDownloadProgressFlow(show) }
downloadStatusJob = launch { updateDownloadStatusFlow(show) }
playbackPositionJob = launch { updatePlaybackPositionPercentFlow(show) }
videoPathJob = launch { getCompletetlyDownloadedVideoPathFlow(show) }
}
private suspend fun updateDownloadProgressFlow(show: PersistedMediathekShow) {
mediathekRepository
.getDownloadProgress(show.id)
.collectLatest(::onDownloadProgressChanged)
}
private suspend fun updateDownloadStatusFlow(show: PersistedMediathekShow) {
mediathekRepository
.getDownloadStatus(show.id)
.collectLatest(::onDownloadStatusChanged)
}
private suspend fun updatePlaybackPositionPercentFlow(show: PersistedMediathekShow) {
mediathekRepository
.getPlaybackPositionPercent(show.mediathekShow.apiId)
.collectLatest(::onPlaybackPositionChanged)
}
private suspend fun getCompletetlyDownloadedVideoPathFlow(show: PersistedMediathekShow) {
mediathekRepository
.getCompletetlyDownloadedVideoPath(show.id)
.collectLatest(::onVideoPathChanged)
}
private fun onDownloadProgressChanged(downloadProgress: Int) {
binding.downloadProgress.progress = downloadProgress
}
private fun onDownloadStatusChanged(status: DownloadStatus) {
when (status) {
DownloadStatus.ADDED, DownloadStatus.QUEUED -> {
binding.icon.setImageDrawable(null)
}
DownloadStatus.DOWNLOADING -> {
binding.icon.setImageDrawable(null)
}
DownloadStatus.COMPLETED -> {
binding.icon.setImageDrawable(null)
}
DownloadStatus.FAILED -> {
binding.icon.setImageResource(R.drawable.ic_outline_warning_amber_24)
}
else -> {
binding.icon.setImageResource(R.drawable.ic_baseline_help_outline_24)
}
}
binding.downloadProgress.isVisible = status == DownloadStatus.QUEUED ||
status == DownloadStatus.DOWNLOADING ||
status == DownloadStatus.PAUSED ||
status == DownloadStatus.ADDED
binding.downloadProgress.isIndeterminate = status != DownloadStatus.DOWNLOADING
}
private fun onPlaybackPositionChanged(playBackPercent: Float) {
binding.viewingProgress.scaleX = playBackPercent
}
private suspend fun onVideoPathChanged(videoPath: String?) {
loadThumbnail(videoPath)
}
private suspend fun loadThumbnail(path: String?) = coroutineScope {
if (path == null) {
updateThumbnail(null)
return@coroutineScope
}
try {
val thumbnail =
ImageHelper.loadThumbnailAsync(binding.root.context, path)
updateThumbnail(thumbnail)
} catch (e: Exception) {
onLoadThumbnailError()
}
}
private fun onLoadThumbnailError() {
binding.thumbnail.setImageResource(R.drawable.ic_sad_tv)
binding.thumbnail.imageAlpha = 28
binding.thumbnail.scaleType = ImageView.ScaleType.CENTER_INSIDE
}
private fun updateThumbnail(thumbnail: Bitmap?) {
binding.thumbnail.setImageBitmap(thumbnail)
binding.thumbnail.imageAlpha = 255
binding.thumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
}
}

View File

@ -20,7 +20,7 @@ class MediathekItemAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediathekItemViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = MediathekListFragmentItemBinding.inflate(inflater, parent, false)
return MediathekItemViewHolder(binding)
return MediathekItemViewHolder(binding, MediathekItemType.Default)
}
override fun onBindViewHolder(holder: MediathekItemViewHolder, position: Int) {

View File

@ -0,0 +1,20 @@
package de.christinecoenen.code.zapp.app.mediathek.ui.list.adapter
/**
* Type of mediathek list item that should be displayed.
* The list item can be slightly adjusted, depending on the type.
*/
enum class MediathekItemType {
/**
* General purpose.
* Items relevant to the user will be highlighted.
* No thumbnail will be loaded.
*/
Default,
/**
* For shows in the downloads list.
* A thumbnail will be loaded.
*/
Download
}

View File

@ -1,6 +1,8 @@
package de.christinecoenen.code.zapp.app.mediathek.ui.list.adapter
import android.graphics.Bitmap
import android.view.View
import android.widget.ImageView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import de.christinecoenen.code.zapp.R
@ -9,16 +11,15 @@ import de.christinecoenen.code.zapp.models.shows.DownloadStatus
import de.christinecoenen.code.zapp.models.shows.MediathekShow
import de.christinecoenen.code.zapp.repositories.MediathekRepository
import de.christinecoenen.code.zapp.utils.system.ColorHelper.themeColor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import de.christinecoenen.code.zapp.utils.system.ImageHelper
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class MediathekItemViewHolder(
private val binding: MediathekListFragmentItemBinding
private val binding: MediathekListFragmentItemBinding,
private val itemType: MediathekItemType,
) : RecyclerView.ViewHolder(binding.root), KoinComponent {
private val mediathekRepository: MediathekRepository by inject()
@ -30,6 +31,7 @@ class MediathekItemViewHolder(
private var downloadProgressJob: Job? = null
private var downloadStatusJob: Job? = null
private var playbackPositionJob: Job? = null
private var videoPathJob: Job? = null
suspend fun setShow(show: MediathekShow) = withContext(Dispatchers.Main) {
binding.root.visibility = View.GONE
@ -38,12 +40,15 @@ class MediathekItemViewHolder(
downloadProgressJob?.cancel()
downloadStatusJob?.cancel()
playbackPositionJob?.cancel()
videoPathJob?.cancel()
binding.title.text = show.title
binding.topic.text = show.topic
// fix layout_constraintWidth_max not be applied correctly
binding.topic.requestLayout()
binding.thumbnail.isVisible = itemType == MediathekItemType.Download
binding.duration.text = show.formattedDuration
binding.channel.text = show.channel
binding.time.text = show.formattedTimestamp
@ -60,10 +65,18 @@ class MediathekItemViewHolder(
binding.root.visibility = View.VISIBLE
isRelevantForUserJob = launch { getIsRelevantForUserFlow(show) }
if (itemType == MediathekItemType.Default) {
isRelevantForUserJob = launch { getIsRelevantForUserFlow(show) }
}
if (itemType == MediathekItemType.Download) {
updateThumbnail(null)
videoPathJob = launch { getCompletetlyDownloadedVideoPathFlow(show) }
}
downloadProgressJob = launch { updateDownloadProgressFlow(show) }
downloadStatusJob = launch { updateDownloadStatusFlow(show) }
playbackPositionJob = launch { updatePlaybackPositionPercentFlow(show) }
downloadStatusJob = launch { updateDownloadStatusFlow(show) }
}
private suspend fun getIsRelevantForUserFlow(show: MediathekShow) {
@ -90,6 +103,12 @@ class MediathekItemViewHolder(
.collectLatest(::updatePlaybackPositionPercentFlow)
}
private suspend fun getCompletetlyDownloadedVideoPathFlow(show: MediathekShow) {
mediathekRepository
.getCompletetlyDownloadedVideoPath(show.apiId)
.collectLatest(::onVideoPathChanged)
}
private fun updateIsRelevantForUser(isRelevant: Boolean) {
binding.root.setBackgroundColor(if (isRelevant) bgColorHighlight else bgColorDefault)
}
@ -124,4 +143,36 @@ class MediathekItemViewHolder(
binding.viewingProgress.progress = (percent * binding.viewingProgress.max).toInt()
binding.viewingProgress.isVisible = percent > 0
}
private suspend fun onVideoPathChanged(videoPath: String?) {
loadThumbnail(videoPath)
}
private suspend fun loadThumbnail(path: String?) = coroutineScope {
if (path == null) {
updateThumbnail(null)
return@coroutineScope
}
try {
val thumbnail = ImageHelper.loadThumbnailAsync(binding.root.context, path)
updateThumbnail(thumbnail)
} catch (e: CancellationException) {
// this is fine - view will be recycled
} catch (e: Exception) {
onLoadThumbnailError()
}
}
private fun onLoadThumbnailError() {
binding.thumbnail.setImageResource(R.drawable.ic_sad_tv)
binding.thumbnail.imageAlpha = 28
binding.thumbnail.scaleType = ImageView.ScaleType.CENTER_INSIDE
}
private fun updateThumbnail(thumbnail: Bitmap?) {
binding.thumbnail.setImageBitmap(thumbnail)
binding.thumbnail.imageAlpha = 255
binding.thumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
}
}

View File

@ -6,8 +6,9 @@ import android.view.ViewGroup
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import de.christinecoenen.code.zapp.app.downloads.ui.list.adapter.DownloadViewHolder
import de.christinecoenen.code.zapp.databinding.DownloadsFragmentListItemBinding
import de.christinecoenen.code.zapp.app.mediathek.ui.list.adapter.MediathekItemType
import de.christinecoenen.code.zapp.app.mediathek.ui.list.adapter.MediathekItemViewHolder
import de.christinecoenen.code.zapp.databinding.MediathekListFragmentItemBinding
import de.christinecoenen.code.zapp.models.shows.PersistedMediathekShow
import de.christinecoenen.code.zapp.utils.view.PersistedMediathekShowDiffUtilCallback
import kotlinx.coroutines.launch
@ -15,7 +16,7 @@ import kotlinx.coroutines.launch
class DownloadListAdapter(
private val scope: LifecycleCoroutineScope,
private val listener: Listener? = null
) : RecyclerView.Adapter<DownloadViewHolder>() {
) : RecyclerView.Adapter<MediathekItemViewHolder>() {
private var persistedShows = mutableListOf<PersistedMediathekShow>()
@ -29,10 +30,10 @@ class DownloadListAdapter(
diffResult.dispatchUpdatesTo(this)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediathekItemViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = DownloadsFragmentListItemBinding.inflate(layoutInflater, parent, false)
val holder = DownloadViewHolder(binding)
val binding = MediathekListFragmentItemBinding.inflate(layoutInflater, parent, false)
val holder = MediathekItemViewHolder(binding, MediathekItemType.Download)
binding.root.setOnClickListener {
listener?.onShowClicked(persistedShows[holder.bindingAdapterPosition])
@ -49,9 +50,9 @@ class DownloadListAdapter(
return holder
}
override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) {
override fun onBindViewHolder(holder: MediathekItemViewHolder, position: Int) {
scope.launch {
holder.bindItem(persistedShows[holder.bindingAdapterPosition])
holder.setShow(persistedShows[holder.bindingAdapterPosition].mediathekShow)
}
}

View File

@ -96,8 +96,8 @@ interface MediathekShowDao {
@Query("SELECT (CAST(playbackPosition AS FLOAT) / videoDuration) FROM PersistedMediathekShow WHERE apiId=:apiId")
fun getPlaybackPositionPercent(apiId: String): Flow<Float>
@Query("SELECT downloadedVideoPath FROM PersistedMediathekShow WHERE id=:id AND downloadStatus=4")
fun getCompletetlyDownloadedVideoPath(id: Int): Flow<String?>
@Query("SELECT downloadedVideoPath FROM PersistedMediathekShow WHERE apiId=:apiId AND downloadStatus=4")
fun getCompletetlyDownloadedVideoPath(apiId: String): Flow<String?>
@Delete
suspend fun delete(show: PersistedMediathekShow)

View File

@ -168,10 +168,10 @@ class MediathekRepository(private val database: Database) {
.flowOn(Dispatchers.IO)
}
fun getCompletetlyDownloadedVideoPath(showId: Int): Flow<String?> {
fun getCompletetlyDownloadedVideoPath(apiId: String): Flow<String?> {
return database
.mediathekShowDao()
.getCompletetlyDownloadedVideoPath(showId)
.getCompletetlyDownloadedVideoPath(apiId)
.distinctUntilChanged()
.flowOn(Dispatchers.IO)
}

View File

@ -13,6 +13,23 @@
tools:ignore="UnusedAttribute"
tools:visibility="visible">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/thumbnail"
android:layout_width="80dp"
android:layout_height="0dp"
android:layout_marginEnd="4dp"
android:background="?colorSurface"
android:backgroundTint="?colorSurfaceVariant"
android:scaleType="centerCrop"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/topic"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/topic"
android:layout_width="0dp"
@ -30,7 +47,7 @@
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_weight="5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/thumbnail"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintWidth_max="wrap"
@ -111,7 +128,7 @@
app:layout_constraintBottom_toBottomOf="@id/topic"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/viewing_status"
app:layout_constraintTop_toTopOf="@id/topic"/>
app:layout_constraintTop_toTopOf="@id/topic" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"