mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
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:
parent
0b69793d7c
commit
a388c57563
4 changed files with 164 additions and 101 deletions
|
@ -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,112 +94,128 @@ 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)
|
||||||
val d = card.data
|
}
|
||||||
|
}
|
||||||
|
|
||||||
downloadHeaderPoster.apply {
|
@SuppressLint("SetTextI18n")
|
||||||
setImage(d.poster)
|
private fun bindHeader(card: VisualDownloadHeaderCached?) {
|
||||||
setOnClickListener {
|
if (binding !is DownloadHeaderEpisodeBinding) return
|
||||||
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
|
card ?: return
|
||||||
}
|
val d = card.data
|
||||||
}
|
|
||||||
|
|
||||||
downloadHeaderTitle.text = d.name
|
binding.apply {
|
||||||
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
|
downloadHeaderPoster.apply {
|
||||||
|
setImage(d.poster)
|
||||||
if (card.child != null) {
|
setOnClickListener {
|
||||||
downloadHeaderGotoChild.isVisible = false
|
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
|
||||||
|
|
||||||
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback)
|
|
||||||
downloadButton.setProgress(card.currentBytes, card.totalBytes)
|
|
||||||
downloadButton.isVisible = true
|
|
||||||
|
|
||||||
episodeHolder.setOnClickListener {
|
|
||||||
mediaClickCallback.invoke(
|
|
||||||
DownloadClickEvent(
|
|
||||||
DOWNLOAD_ACTION_PLAY_FILE,
|
|
||||||
card.child
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
downloadButton.isVisible = false
|
|
||||||
downloadHeaderGotoChild.isVisible = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
downloadHeaderInfo.text =
|
|
||||||
downloadHeaderInfo.context.getString(R.string.extra_info_format)
|
|
||||||
.format(
|
|
||||||
card.totalDownloads,
|
|
||||||
if (card.totalDownloads == 1) downloadHeaderInfo.context.getString(
|
|
||||||
R.string.episode
|
|
||||||
) else downloadHeaderInfo.context.getString(
|
|
||||||
R.string.episodes
|
|
||||||
),
|
|
||||||
mbString
|
|
||||||
)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
// You probably formatted incorrectly
|
|
||||||
downloadHeaderInfo.text = "Error"
|
|
||||||
logError(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
episodeHolder.setOnClickListener {
|
|
||||||
clickCallback.invoke(DownloadHeaderClickEvent(0, d))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadChildEpisodeBinding -> binding.apply {
|
downloadHeaderTitle.text = d.name
|
||||||
if (card == null || card !is VisualDownloadChildCached) return@apply
|
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
|
||||||
val d = card.data
|
|
||||||
|
|
||||||
val posDur = DataStoreHelper.getViewPos(d.id)
|
if (card.child != null) {
|
||||||
downloadChildEpisodeProgress.apply {
|
downloadHeaderGotoChild.isVisible = false
|
||||||
if (posDur != null) {
|
|
||||||
val visualPos = posDur.fixVisual()
|
val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes)
|
||||||
max = (visualPos.duration / 1000).toInt()
|
if (status == DownloadStatusTell.IsDone) {
|
||||||
progress = (visualPos.position / 1000).toInt()
|
// We do this here instead if we are finished downloading
|
||||||
isVisible = true
|
// so that we can use the value from the view model
|
||||||
} else isVisible = false
|
// rather than extra unneeded disk operations and to prevent a
|
||||||
|
// delay in updating download icon state.
|
||||||
|
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
|
||||||
|
|
||||||
|
episodeHolder.setOnClickListener {
|
||||||
|
mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
downloadButton.isVisible = false
|
||||||
|
downloadHeaderGotoChild.isVisible = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
downloadHeaderInfo.text = downloadHeaderInfo.context.getString(R.string.extra_info_format)
|
||||||
|
.format(
|
||||||
|
card.totalDownloads,
|
||||||
|
downloadHeaderInfo.context.resources.getQuantityString(
|
||||||
|
R.plurals.episodes,
|
||||||
|
card.totalDownloads
|
||||||
|
),
|
||||||
|
mbString
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// You probably formatted incorrectly
|
||||||
|
downloadHeaderInfo.text = "Error"
|
||||||
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadButton.setDefaultClickListener(card.data, downloadChildEpisodeTextExtra, mediaClickCallback)
|
episodeHolder.setOnClickListener {
|
||||||
|
clickCallback.invoke(DownloadHeaderClickEvent(0, d))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
downloadChildEpisodeText.apply {
|
downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback)
|
||||||
text = context.getNameFull(d.name, d.episode, d.season)
|
downloadButton.isVisible = true
|
||||||
isSelected = true // Needed for text repeating
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadChildEpisodeHolder.setOnClickListener {
|
downloadChildEpisodeText.apply {
|
||||||
mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d))
|
text = context.getNameFull(d.name, d.episode, d.season)
|
||||||
}
|
isSelected = true // Needed for text repeating
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadChildEpisodeHolder.setOnClickListener {
|
||||||
|
mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>() {
|
||||||
|
|
|
@ -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,13 +137,16 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
if (isZeroBytes) {
|
if (isZeroBytes) {
|
||||||
progressText?.isVisible = false
|
progressText?.isVisible = false
|
||||||
} else {
|
} else {
|
||||||
progressText?.apply {
|
if (setProgressText) {
|
||||||
val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes)
|
progressText?.apply {
|
||||||
val totalMbString = Formatter.formatShortFileSize(context, totalBytes)
|
val currentMbString =
|
||||||
text =
|
Formatter.formatShortFileSize(context, downloadedBytes)
|
||||||
//if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
|
val totalMbString = Formatter.formatShortFileSize(context, totalBytes)
|
||||||
context?.getString(R.string.download_size_format)
|
text =
|
||||||
?.format(currentMbString, totalMbString)
|
//if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
|
||||||
|
context?.getString(R.string.download_size_format)
|
||||||
|
?.format(currentMbString, totalMbString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue