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:
parent
216c6137fe
commit
49e4cfbc1e
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user