Cleanup adapter, and rely more on view model + UI improvements + run more in IO context

This makes it use the view model more when in downloads view, because otherwise when running savedData in an IO context it causes it to wait before updating progress which causes visible UI delays. This prevents that with downloads with the IsDone status.

This also has a UI change: If a download is complete, it no longer shows bytes / bytes, it only shows bytes once. This is both a UI and performance improvement to have to do less. Finally, this does some cleanup to DownloadAdapter.
This commit is contained in:
Luna712 2024-07-01 17:27:31 -06:00 committed by GitHub
parent 0b69793d7c
commit a388c57563
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 164 additions and 101 deletions

View file

@ -13,6 +13,7 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding
import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
@ -93,13 +94,20 @@ class DownloadAdapter(
private val mediaClickCallback: (DownloadClickEvent) -> Unit, private val mediaClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(card: VisualDownloadCached?) { fun bind(card: VisualDownloadCached?) {
when (binding) { when (binding) {
is DownloadHeaderEpisodeBinding -> binding.apply { is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadHeaderCached)
if (card == null || card !is VisualDownloadHeaderCached) return@apply is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadChildCached)
}
}
@SuppressLint("SetTextI18n")
private fun bindHeader(card: VisualDownloadHeaderCached?) {
if (binding !is DownloadHeaderEpisodeBinding) return
card ?: return
val d = card.data val d = card.data
binding.apply {
downloadHeaderPoster.apply { downloadHeaderPoster.apply {
setImage(d.poster) setImage(d.poster)
setOnClickListener { setOnClickListener {
@ -113,38 +121,43 @@ class DownloadAdapter(
if (card.child != null) { if (card.child != null) {
downloadHeaderGotoChild.isVisible = false downloadHeaderGotoChild.isVisible = false
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes)
if (status == DownloadStatusTell.IsDone) {
// We do this here instead if we are finished downloading
// so that we can use the value from the view model
// rather than extra unneeded disk operations and to prevent a
// delay in updating download icon state.
downloadButton.setProgress(card.currentBytes, card.totalBytes) downloadButton.setProgress(card.currentBytes, card.totalBytes)
downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes)
// We will let the view model handle this
downloadButton.setProgressText = false
downloadHeaderInfo.text = formatShortFileSize(downloadHeaderInfo.context, card.totalBytes)
} else downloadButton.setProgressText = true
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback)
downloadButton.isVisible = true downloadButton.isVisible = true
episodeHolder.setOnClickListener { episodeHolder.setOnClickListener {
mediaClickCallback.invoke( mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child))
DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE,
card.child
)
)
} }
} else { } else {
downloadButton.isVisible = false downloadButton.isVisible = false
downloadHeaderGotoChild.isVisible = true downloadHeaderGotoChild.isVisible = true
try { try {
downloadHeaderInfo.text = downloadHeaderInfo.text = downloadHeaderInfo.context.getString(R.string.extra_info_format)
downloadHeaderInfo.context.getString(R.string.extra_info_format)
.format( .format(
card.totalDownloads, card.totalDownloads,
if (card.totalDownloads == 1) downloadHeaderInfo.context.getString( downloadHeaderInfo.context.resources.getQuantityString(
R.string.episode R.plurals.episodes,
) else downloadHeaderInfo.context.getString( card.totalDownloads
R.string.episodes
), ),
mbString mbString
) )
} catch (t: Throwable) { } catch (e: Exception) {
// You probably formatted incorrectly // You probably formatted incorrectly
downloadHeaderInfo.text = "Error" downloadHeaderInfo.text = "Error"
logError(t) logError(e)
} }
episodeHolder.setOnClickListener { episodeHolder.setOnClickListener {
@ -152,23 +165,39 @@ class DownloadAdapter(
} }
} }
} }
is DownloadChildEpisodeBinding -> binding.apply {
if (card == null || card !is VisualDownloadChildCached) return@apply
val d = card.data
val posDur = DataStoreHelper.getViewPos(d.id)
downloadChildEpisodeProgress.apply {
if (posDur != null) {
val visualPos = posDur.fixVisual()
max = (visualPos.duration / 1000).toInt()
progress = (visualPos.position / 1000).toInt()
isVisible = true
} else isVisible = false
} }
downloadButton.setDefaultClickListener(card.data, downloadChildEpisodeTextExtra, mediaClickCallback) private fun bindChild(card: VisualDownloadChildCached?) {
if (binding !is DownloadChildEpisodeBinding) return
card ?: return
val d = card.data
binding.apply {
val posDur = DataStoreHelper.getViewPos(d.id)
downloadChildEpisodeProgress.apply {
isVisible = posDur != null
posDur?.let {
val visualPos = it.fixVisual()
max = (visualPos.duration / 1000).toInt()
progress = (visualPos.position / 1000).toInt()
}
}
val status = downloadButton.getStatus(d.id, card.currentBytes, card.totalBytes)
if (status == DownloadStatusTell.IsDone) {
// We do this here instead if we are finished downloading
// so that we can use the value from the view model
// rather than extra unneeded disk operations and to prevent a
// delay in updating download icon state.
downloadButton.setProgress(card.currentBytes, card.totalBytes) downloadButton.setProgress(card.currentBytes, card.totalBytes)
downloadButton.applyMetaData(d.id, card.currentBytes, card.totalBytes)
// We will let the view model handle this
downloadButton.setProgressText = false
downloadChildEpisodeTextExtra.text = formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes)
} else downloadButton.setProgressText = true
downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback)
downloadButton.isVisible = true
downloadChildEpisodeText.apply { downloadChildEpisodeText.apply {
text = context.getNameFull(d.name, d.episode, d.season) text = context.getNameFull(d.name, d.episode, d.season)
@ -181,24 +210,12 @@ class DownloadAdapter(
} }
} }
} }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = when (viewType) { val binding = when (viewType) {
VIEW_TYPE_HEADER -> { VIEW_TYPE_HEADER -> DownloadHeaderEpisodeBinding.inflate(inflater, parent, false)
DownloadHeaderEpisodeBinding.inflate( VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false)
LayoutInflater.from(parent.context),
parent,
false
)
}
VIEW_TYPE_CHILD -> {
DownloadChildEpisodeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
}
else -> throw IllegalArgumentException("Invalid view type") else -> throw IllegalArgumentException("Invalid view type")
} }
return DownloadViewHolder(binding, clickCallback, mediaClickCallback) return DownloadViewHolder(binding, clickCallback, mediaClickCallback)
@ -209,8 +226,11 @@ class DownloadAdapter(
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
val card = getItem(position) return when (getItem(position)) {
return if (card is VisualDownloadChildCached) VIEW_TYPE_CHILD else VIEW_TYPE_HEADER is VisualDownloadChildCached -> VIEW_TYPE_CHILD
is VisualDownloadHeaderCached -> VIEW_TYPE_HEADER
else -> throw IllegalArgumentException("Invalid data type at position $position")
}
} }
class DiffCallback : DiffUtil.ItemCallback<VisualDownloadCached>() { class DiffCallback : DiffUtil.ItemCallback<VisualDownloadCached>() {

View file

@ -9,6 +9,8 @@ import androidx.annotation.LayoutRes
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar import androidx.core.widget.ContentLoadingProgressBar
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
typealias DownloadStatusTell = VideoDownloadManager.DownloadType typealias DownloadStatusTell = VideoDownloadManager.DownloadType
@ -64,11 +66,42 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
var currentMetaData: DownloadMetadata = var currentMetaData: DownloadMetadata =
DownloadMetadata(0, 0, 0, null) DownloadMetadata(0, 0, 0, null)
private var progressSet = false
var setProgressText = true
fun setPersistentId(id: Int) {
persistentId = id
currentMetaData.id = id
if (progressSet) return
ioSafe {
val savedData = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, id)
mainWork {
if (savedData != null) {
val downloadedBytes = savedData.fileLength
val totalBytes = savedData.totalBytes
setProgress(downloadedBytes, totalBytes)
applyMetaData(id, downloadedBytes, totalBytes)
} else run { resetView() }
}
}
}
abstract fun setStatus(status: VideoDownloadManager.DownloadType?) abstract fun setStatus(status: VideoDownloadManager.DownloadType?)
open fun setProgress(downloadedBytes: Long, totalBytes: Long) { fun getStatus(id:Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell {
val status = VideoDownloadManager.downloadStatus[id] return VideoDownloadManager.downloadStatus[id]
?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) DownloadStatusTell.IsDone else DownloadStatusTell.IsPaused ?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) {
DownloadStatusTell.IsDone
} else DownloadStatusTell.IsPaused
}
fun applyMetaData(id:Int, downloadedBytes: Long, totalBytes: Long) {
val status = getStatus(id, downloadedBytes, totalBytes)
currentMetaData.apply { currentMetaData.apply {
this.id = id this.id = id
this.downloadedLength = downloadedBytes this.downloadedLength = downloadedBytes
@ -76,6 +109,10 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
this.status = status this.status = status
} }
setStatus(status) setStatus(status)
}
open fun setProgress(downloadedBytes: Long, totalBytes: Long) {
progressSet = true
isZeroBytes = downloadedBytes == 0L isZeroBytes = downloadedBytes == 0L
progressBar.post { progressBar.post {
val steps = 10000L val steps = 10000L
@ -100,8 +137,10 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
if (isZeroBytes) { if (isZeroBytes) {
progressText?.isVisible = false progressText?.isVisible = false
} else { } else {
if (setProgressText) {
progressText?.apply { progressText?.apply {
val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes) val currentMbString =
Formatter.formatShortFileSize(context, downloadedBytes)
val totalMbString = Formatter.formatShortFileSize(context, totalBytes) val totalMbString = Formatter.formatShortFileSize(context, totalBytes)
text = text =
//if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else //if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
@ -109,6 +148,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
?.format(currentMbString, totalMbString) ?.format(currentMbString, totalMbString)
} }
} }
}
progressBar.startAnimation(animation) progressBar.startAnimation(animation)
} }
@ -150,7 +190,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
val pid = persistentId val pid = persistentId
if (pid != null) { if (pid != null) {
// refresh in case of onDetachedFromWindow -> onAttachedToWindow while still being ??????? // refresh in case of onDetachedFromWindow -> onAttachedToWindow while still being ???????
currentMetaData.id = pid setPersistentId(pid)
} }
super.onAttachedToWindow() super.onAttachedToWindow()

View file

@ -13,7 +13,6 @@ import androidx.annotation.MainThread
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -29,7 +28,6 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES
open class PieFetchButton(context: Context, attributeSet: AttributeSet) : open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
BaseFetchButton(context, attributeSet) { BaseFetchButton(context, attributeSet) {
@ -167,6 +165,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
callback: (DownloadClickEvent) -> Unit callback: (DownloadClickEvent) -> Unit
) { ) {
this.progressText = textView this.progressText = textView
this.setPersistentId(card.id)
view.setOnClickListener { view.setOnClickListener {
if (isZeroBytes) { if (isZeroBytes) {
removeKey(KEY_RESUME_PACKAGES, card.id.toString()) removeKey(KEY_RESUME_PACKAGES, card.id.toString())

View file

@ -339,6 +339,10 @@
<string name="livestreams">Livestreams</string> <string name="livestreams">Livestreams</string>
<string name="nsfw">NSFW</string> <string name="nsfw">NSFW</string>
<string name="others">Others</string> <string name="others">Others</string>
<plurals name="episodes">
<item quantity="one">Episode</item>
<item quantity="other">Episodes</item>
</plurals>
<!--singular--> <!--singular-->
<string name="movies_singular">Movie</string> <string name="movies_singular">Movie</string>
<string name="tv_series_singular">Series</string> <string name="tv_series_singular">Series</string>