Downloads: performance improvements and merge adapters (#1145)

This commit is contained in:
Luna712 2024-06-24 12:04:45 -06:00 committed by GitHub
parent b9746c2b17
commit b06d9f224d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 488 additions and 527 deletions

View file

@ -0,0 +1,223 @@
package com.lagradost.cloudstream3.ui.download
import android.annotation.SuppressLint
import android.text.format.Formatter.formatShortFileSize
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding
import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
const val DOWNLOAD_ACTION_PLAY_FILE = 0
const val DOWNLOAD_ACTION_DELETE_FILE = 1
const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2
const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3
const val DOWNLOAD_ACTION_DOWNLOAD = 4
const val DOWNLOAD_ACTION_LONG_CLICK = 5
abstract class VisualDownloadCached(
open val currentBytes: Long,
open val totalBytes: Long,
open val data: VideoDownloadHelper.DownloadCached
) {
// Just to be extra-safe with areContentsTheSame
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is VisualDownloadCached) return false
if (currentBytes != other.currentBytes) return false
if (totalBytes != other.totalBytes) return false
if (data != other.data) return false
return true
}
override fun hashCode(): Int {
var result = currentBytes.hashCode()
result = 31 * result + totalBytes.hashCode()
result = 31 * result + data.hashCode()
return result
}
}
data class VisualDownloadChildCached(
override val currentBytes: Long,
override val totalBytes: Long,
override val data: VideoDownloadHelper.DownloadEpisodeCached,
): VisualDownloadCached(currentBytes, totalBytes, data)
data class VisualDownloadHeaderCached(
override val currentBytes: Long,
override val totalBytes: Long,
override val data: VideoDownloadHelper.DownloadHeaderCached,
val child: VideoDownloadHelper.DownloadEpisodeCached?,
val currentOngoingDownloads: Int,
val totalDownloads: Int,
): VisualDownloadCached(currentBytes, totalBytes, data)
data class DownloadClickEvent(
val action: Int,
val data: VideoDownloadHelper.DownloadEpisodeCached
)
data class DownloadHeaderClickEvent(
val action: Int,
val data: VideoDownloadHelper.DownloadHeaderCached
)
class DownloadAdapter(
private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
private val mediaClickCallback: (DownloadClickEvent) -> Unit,
) : ListAdapter<VisualDownloadCached, DownloadAdapter.DownloadViewHolder>(DiffCallback()) {
companion object {
private const val VIEW_TYPE_HEADER = 0
private const val VIEW_TYPE_CHILD = 1
}
inner class DownloadViewHolder(
private val binding: ViewBinding,
private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
private val mediaClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(card: VisualDownloadCached?) {
when (binding) {
is DownloadHeaderEpisodeBinding -> binding.apply {
if (card == null || card !is VisualDownloadHeaderCached) return@apply
val d = card.data
downloadHeaderPoster.apply {
setImage(d.poster)
setOnClickListener {
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
}
}
downloadHeaderTitle.text = d.name
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
if (card.child != null) {
downloadHeaderGotoChild.isVisible = false
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,
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 {
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)
downloadChildEpisodeText.apply {
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 {
val binding = when (viewType) {
VIEW_TYPE_HEADER -> {
DownloadHeaderEpisodeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
}
VIEW_TYPE_CHILD -> {
DownloadChildEpisodeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
}
else -> throw IllegalArgumentException("Invalid view type")
}
return DownloadViewHolder(binding, clickCallback, mediaClickCallback)
}
override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun getItemViewType(position: Int): Int {
val card = getItem(position)
return if (card is VisualDownloadChildCached) VIEW_TYPE_CHILD else VIEW_TYPE_HEADER
}
class DiffCallback : DiffUtil.ItemCallback<VisualDownloadCached>() {
override fun areItemsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean {
return oldItem.data.id == newItem.data.id
}
override fun areContentsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean {
return oldItem == newItem
}
}
}

View file

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.ui.download package com.lagradost.cloudstream3.ui.download
import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -22,7 +21,6 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager
object DownloadButtonSetup { object DownloadButtonSetup {
fun handleDownloadClick(click: DownloadClickEvent) { fun handleDownloadClick(click: DownloadClickEvent) {
val id = click.data.id val id = click.data.id
if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return
when (click.action) { when (click.action) {
DOWNLOAD_ACTION_DELETE_FILE -> { DOWNLOAD_ACTION_DELETE_FILE -> {
activity?.let { ctx -> activity?.let { ctx ->

View file

@ -1,94 +0,0 @@
package com.lagradost.cloudstream3.ui.download
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
const val DOWNLOAD_ACTION_PLAY_FILE = 0
const val DOWNLOAD_ACTION_DELETE_FILE = 1
const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2
const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3
const val DOWNLOAD_ACTION_DOWNLOAD = 4
const val DOWNLOAD_ACTION_LONG_CLICK = 5
data class VisualDownloadChildCached(
val currentBytes: Long,
val totalBytes: Long,
val data: VideoDownloadHelper.DownloadEpisodeCached,
)
data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached)
class DownloadChildAdapter(
var cardList: List<VisualDownloadChildCached>,
private val clickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return DownloadChildViewHolder(
DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false),
clickCallback
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is DownloadChildViewHolder -> {
holder.bind(cardList[position])
}
}
}
override fun getItemCount(): Int {
return cardList.size
}
class DownloadChildViewHolder
constructor(
val binding: DownloadChildEpisodeBinding,
private val clickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
/*private val title: TextView = itemView.download_child_episode_text
private val extraInfo: TextView = itemView.download_child_episode_text_extra
private val holder: CardView = itemView.download_child_episode_holder
private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress
private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded
private val downloadImage: ImageView = itemView.download_child_episode_download*/
fun bind(card: VisualDownloadChildCached) {
val d = card.data
val posDur = getViewPos(d.id)
binding.downloadChildEpisodeProgress.apply {
if (posDur != null) {
val visualPos = posDur.fixVisual()
max = (visualPos.duration / 1000).toInt()
progress = (visualPos.position / 1000).toInt()
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
binding.downloadButton.setDefaultClickListener(card.data, binding.downloadChildEpisodeTextExtra, clickCallback)
binding.downloadChildEpisodeText.apply {
text = context.getNameFull(d.name, d.episode, d.season)
isSelected = true // is needed for text repeating
}
binding.downloadChildEpisodeHolder.setOnClickListener {
clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d))
}
}
}
}

View file

@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
@ -40,7 +39,8 @@ class DownloadChildFragment : Fragment() {
super.onDestroyView() super.onDestroyView()
} }
var binding: FragmentChildDownloadsBinding? = null private var binding: FragmentChildDownloadsBinding? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -48,7 +48,7 @@ class DownloadChildFragment : Fragment() {
): View { ): View {
val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false)
binding = localBinding binding = localBinding
return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false) return localBinding.root
} }
private fun updateList(folder: String) = main { private fun updateList(folder: String) = main {
@ -60,7 +60,11 @@ class DownloadChildFragment : Fragment() {
}.mapNotNull { }.mapNotNull {
val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id) val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id)
?: return@mapNotNull null ?: return@mapNotNull null
VisualDownloadChildCached(info.fileLength, info.totalBytes, it) VisualDownloadChildCached(
currentBytes = info.fileLength,
totalBytes = info.totalBytes,
data = it,
)
} }
}.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 }
if (eps.isEmpty()) { if (eps.isEmpty()) {
@ -68,9 +72,7 @@ class DownloadChildFragment : Fragment() {
return@main return@main
} }
(binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(eps)
eps
binding?.downloadChildList?.adapter?.notifyDataSetChanged()
} }
} }
@ -98,31 +100,39 @@ class DownloadChildFragment : Fragment() {
setAppBarNoScrollFlagsOnTV() setAppBarNoScrollFlagsOnTV()
} }
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = val adapter = DownloadAdapter(
DownloadChildAdapter( {},
ArrayList(), { downloadClickEvent ->
) { click -> handleDownloadClick(downloadClickEvent)
handleDownloadClick(click) if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
setUpDownloadDeleteListener(folder)
}
}
)
binding?.downloadChildList?.apply {
setHasFixedSize(true)
setItemViewCacheSize(20)
this.adapter = adapter
setLinearListLayout(
isHorizontal = false,
nextRight = FOCUS_SELF,
nextDown = FOCUS_SELF,
)
} }
updateList(folder)
}
private fun setUpDownloadDeleteListener(folder: String) {
downloadDeleteEventListener = { id: Int -> downloadDeleteEventListener = { id: Int ->
val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList val list = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.currentList
if (list != null) { if (list != null) {
if (list.any { it.data.id == id }) { if (list.any { it.data.id == id }) {
updateList(folder) updateList(folder)
} }
} }
} }
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
binding?.downloadChildList?.adapter = adapter
binding?.downloadChildList?.setLinearListLayout(
isHorizontal = false,
nextDown = FOCUS_SELF,
nextRight = FOCUS_SELF
)//layoutManager = GridLayoutManager(context, 1)
updateList(folder)
} }
} }

View file

@ -10,14 +10,15 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
@ -42,11 +43,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
import java.net.URI import java.net.URI
const val DOWNLOAD_NAVIGATE_TO = "downloadpage" const val DOWNLOAD_NAVIGATE_TO = "downloadpage"
class DownloadFragment : Fragment() { class DownloadFragment : Fragment() {
@ -63,33 +62,30 @@ class DownloadFragment : Fragment() {
private fun setList(list: List<VisualDownloadHeaderCached>) { private fun setList(list: List<VisualDownloadHeaderCached>) {
main { main {
(binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list (binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(list)
binding?.downloadList?.adapter?.notifyDataSetChanged()
} }
} }
override fun onDestroyView() { override fun onDestroyView() {
if (downloadDeleteEventListener != null) { downloadDeleteEventListener?.let {
VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!! VideoDownloadManager.downloadDeleteEvent -= it
downloadDeleteEventListener = null
} }
downloadDeleteEventListener = null
binding = null binding = null
super.onDestroyView() super.onDestroyView()
} }
var binding: FragmentDownloadsBinding? = null private var binding: FragmentDownloadsBinding? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
downloadsViewModel = downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
ViewModelProvider(this)[DownloadViewModel::class.java]
val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false) val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false)
binding = localBinding binding = localBinding
return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false) return localBinding.root
} }
private var downloadDeleteEventListener: ((Int) -> Unit)? = null private var downloadDeleteEventListener: ((Int) -> Unit)? = null
@ -97,7 +93,6 @@ class DownloadFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
hideKeyboard() hideKeyboard()
binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV()
observe(downloadsViewModel.noDownloadsText) { observe(downloadsViewModel.noDownloadsText) {
@ -108,135 +103,109 @@ class DownloadFragment : Fragment() {
binding?.downloadLoading?.isVisible = false binding?.downloadLoading?.isVisible = false
} }
observe(downloadsViewModel.availableBytes) { observe(downloadsViewModel.availableBytes) {
binding?.downloadFreeTxt?.text = updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree)
getString(R.string.storage_size_format).format(
getString(R.string.free_storage),
formatShortFileSize(view.context, it)
)
binding?.downloadFree?.setLayoutWidth(it)
} }
observe(downloadsViewModel.usedBytes) { observe(downloadsViewModel.usedBytes) {
binding?.apply { updateStorageInfo(view.context, it, R.string.used_storage, binding?.downloadUsedTxt, binding?.downloadUsed)
downloadUsedTxt.text = binding?.downloadStorageAppbar?.isVisible = it > 0
getString(R.string.storage_size_format).format(
getString(R.string.used_storage),
formatShortFileSize(view.context, it)
)
downloadUsed.setLayoutWidth(it)
downloadStorageAppbar.isVisible = it > 0
}
} }
observe(downloadsViewModel.downloadBytes) { observe(downloadsViewModel.downloadBytes) {
binding?.apply { updateStorageInfo(view.context, it, R.string.app_storage, binding?.downloadAppTxt, binding?.downloadApp)
downloadAppTxt.text =
getString(R.string.storage_size_format).format(
getString(R.string.app_storage),
formatShortFileSize(view.context, it)
)
downloadApp.setLayoutWidth(it)
}
} }
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = val adapter = DownloadAdapter(
DownloadHeaderAdapter(
ArrayList(),
{ click -> { click ->
handleItemClick(click)
},
{ downloadClickEvent ->
handleDownloadClick(downloadClickEvent)
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
setUpDownloadDeleteListener()
}
}
)
binding?.downloadList?.apply {
setHasFixedSize(true)
setItemViewCacheSize(20)
this.adapter = adapter
setLinearListLayout(
isHorizontal = false,
nextRight = FOCUS_SELF,
nextUp = FOCUS_SELF,
nextDown = FOCUS_SELF,
)
}
binding?.downloadStreamButton?.apply {
isGone = isLayout(TV)
setOnClickListener { showStreamInputDialog(it.context) }
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
handleScroll(scrollY - oldScrollY)
}
}
downloadsViewModel.updateList(requireContext())
fixPaddingStatusbar(binding?.downloadRoot)
}
private fun handleItemClick(click: DownloadHeaderClickEvent) {
when (click.action) { when (click.action) {
0 -> { 0 -> {
if (click.data.type.isMovieType()) { if (!click.data.type.isMovieType()) {
//wont be called val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString())
} else {
val folder = DataStore.getFolderName(
DOWNLOAD_EPISODE_CACHE,
click.data.id.toString()
)
activity?.navigate( activity?.navigate(
R.id.action_navigation_downloads_to_navigation_download_child, R.id.action_navigation_downloads_to_navigation_download_child,
DownloadChildFragment.newInstance(click.data.name, folder) DownloadChildFragment.newInstance(click.data.name, folder)
) )
} }
} }
1 -> { 1 -> {
(activity as AppCompatActivity?)?.loadResult( (activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName)
click.data.url, }
click.data.apiName
)
} }
} }
}, private fun setUpDownloadDeleteListener() {
{ downloadClickEvent ->
if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter
handleDownloadClick(downloadClickEvent)
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
context?.let { ctx ->
downloadsViewModel.updateList(ctx)
}
}
}
)
downloadDeleteEventListener = { id -> downloadDeleteEventListener = { id ->
val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList val list = (binding?.downloadList?.adapter as? DownloadAdapter)?.currentList
if (list != null) { if (list?.any { it.data.id == id } == true) {
if (list.any { it.data.id == id }) { context?.let { downloadsViewModel.updateList(it) }
context?.let { ctx ->
setList(ArrayList())
downloadsViewModel.updateList(ctx)
} }
} }
}
}
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
binding?.downloadList?.apply {
this.adapter = adapter
setLinearListLayout(
isHorizontal = false,
nextRight = FOCUS_SELF,
nextUp = FOCUS_SELF,
nextDown = FOCUS_SELF
)
//layoutManager = GridLayoutManager(context, 1)
} }
// Should be visible in emulator layout private fun updateStorageInfo(
binding?.downloadStreamButton?.isGone = isLayout(TV) context: Context,
binding?.downloadStreamButton?.setOnClickListener { bytes: Long,
val dialog = @StringRes stringRes: Int,
Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) textView: TextView?,
view: View?
) {
textView?.text = getString(R.string.storage_size_format).format(getString(stringRes), formatShortFileSize(context, bytes))
view?.setLayoutWidth(bytes)
}
private fun showStreamInputDialog(context: Context) {
val dialog = Dialog(context, R.style.AlertDialogCustom)
val binding = StreamInputBinding.inflate(dialog.layoutInflater) val binding = StreamInputBinding.inflate(dialog.layoutInflater)
dialog.setContentView(binding.root) dialog.setContentView(binding.root)
dialog.show() dialog.show()
// If user has clicked the switch do not interfere
var preventAutoSwitching = false var preventAutoSwitching = false
binding.hlsSwitch.setOnClickListener { binding.hlsSwitch.setOnClickListener { preventAutoSwitching = true }
preventAutoSwitching = true
}
fun activateSwitchOnHls(text: String?) {
binding.hlsSwitch.isChecked = normalSafeApiCall {
URI(text).path?.substringAfterLast(".")?.contains("m3u")
} == true
}
binding.streamReferer.doOnTextChanged { text, _, _, _ -> binding.streamReferer.doOnTextChanged { text, _, _, _ ->
if (!preventAutoSwitching) if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding)
activateSwitchOnHls(text?.toString())
} }
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt( (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(0)?.text?.toString()?.let { copy ->
0
)?.text?.toString()?.let { copy ->
val fixedText = copy.trim() val fixedText = copy.trim()
binding.streamUrl.setText(fixedText) binding.streamUrl.setText(fixedText)
activateSwitchOnHls(fixedText) activateSwitchOnHls(fixedText, binding)
} }
binding.applyBtt.setOnClickListener { binding.applyBtt.setOnClickListener {
@ -245,7 +214,6 @@ class DownloadFragment : Fragment() {
showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT)
} else { } else {
val referer = binding.streamReferer.text?.toString() val referer = binding.streamReferer.text?.toString()
activity?.navigate( activity?.navigate(
R.id.global_to_navigation_player, R.id.global_to_navigation_player,
GeneratorPlayer.newInstance( GeneratorPlayer.newInstance(
@ -257,7 +225,6 @@ class DownloadFragment : Fragment() {
) )
) )
) )
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
} }
@ -266,18 +233,18 @@ class DownloadFragment : Fragment() {
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
val dy = scrollY - oldScrollY
if (dy > 0) { //check for scroll down
binding?.downloadStreamButton?.shrink() // hide
} else if (dy < -5) {
binding?.downloadStreamButton?.extend() // show
}
}
}
downloadsViewModel.updateList(requireContext())
fixPaddingStatusbar(binding?.downloadRoot) private fun activateSwitchOnHls(text: String?, binding: StreamInputBinding) {
binding.hlsSwitch.isChecked = normalSafeApiCall {
URI(text).path?.substringAfterLast(".")?.contains("m3u")
} == true
}
private fun handleScroll(dy: Int) {
if (dy > 0) {
binding?.downloadStreamButton?.shrink()
} else if (dy < -5) {
binding?.downloadStreamButton?.extend()
}
} }
} }

View file

@ -1,149 +0,0 @@
package com.lagradost.cloudstream3.ui.download
import android.annotation.SuppressLint
import android.text.format.Formatter.formatShortFileSize
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import java.util.*
data class VisualDownloadHeaderCached(
val currentOngoingDownloads: Int,
val totalDownloads: Int,
val totalBytes: Long,
val currentBytes: Long,
val data: VideoDownloadHelper.DownloadHeaderCached,
val child: VideoDownloadHelper.DownloadEpisodeCached?,
)
data class DownloadHeaderClickEvent(
val action: Int,
val data: VideoDownloadHelper.DownloadHeaderCached
)
class DownloadHeaderAdapter(
var cardList: List<VisualDownloadHeaderCached>,
private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
private val movieClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return DownloadHeaderViewHolder(
DownloadHeaderEpisodeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
clickCallback,
movieClickCallback
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is DownloadHeaderViewHolder -> {
holder.bind(cardList[position])
}
}
}
override fun getItemCount(): Int {
return cardList.size
}
class DownloadHeaderViewHolder
constructor(
val binding: DownloadHeaderEpisodeBinding,
private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
private val movieClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
/*private val poster: ImageView? = itemView.download_header_poster
private val title: TextView = itemView.download_header_title
private val extraInfo: TextView = itemView.download_header_info
private val holder: CardView = itemView.episode_holder
private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded
private val downloadImage: ImageView = itemView.download_header_episode_download
private val normalImage: ImageView = itemView.download_header_goto_child*/
@SuppressLint("SetTextI18n")
fun bind(card: VisualDownloadHeaderCached) {
val d = card.data
binding.downloadHeaderPoster.apply {
setImage(d.poster)
setOnClickListener {
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
}
}
binding.apply {
binding.downloadHeaderTitle.text = d.name
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
//val isMovie = d.type.isMovieType()
if (card.child != null) {
//downloadHeaderProgressDownloaded.visibility = View.VISIBLE
// downloadHeaderEpisodeDownload.visibility = View.VISIBLE
binding.downloadHeaderGotoChild.visibility = View.GONE
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback)
downloadButton.isVisible = true
/*setUpButton(
card.currentBytes,
card.totalBytes,
downloadBar,
downloadImage,
extraInfo,
card.child,
movieClickCallback
)*/
episodeHolder.setOnClickListener {
movieClickCallback.invoke(
DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE,
card.child
)
)
}
} else {
downloadButton.isVisible = false
// downloadHeaderProgressDownloaded.visibility = View.GONE
// downloadHeaderEpisodeDownload.visibility = View.GONE
binding.downloadHeaderGotoChild.visibility = View.VISIBLE
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))
}
}
}
}
}
}

View file

@ -39,6 +39,8 @@ class DownloadViewModel : ViewModel() {
val availableBytes: LiveData<Long> = _availableBytes val availableBytes: LiveData<Long> = _availableBytes
val downloadBytes: LiveData<Long> = _downloadBytes val downloadBytes: LiveData<Long> = _downloadBytes
private var previousVisual: List<VisualDownloadHeaderCached>? = null
fun updateList(context: Context) = viewModelScope.launchSafe { fun updateList(context: Context) = viewModelScope.launchSafe {
val children = withContext(Dispatchers.IO) { val children = withContext(Dispatchers.IO) {
val headers = context.getKeys(DOWNLOAD_EPISODE_CACHE) val headers = context.getKeys(DOWNLOAD_EPISODE_CACHE)
@ -53,7 +55,6 @@ class DownloadViewModel : ViewModel() {
// parentId : downloadsCount // parentId : downloadsCount
val totalDownloads = HashMap<Int, Int>() val totalDownloads = HashMap<Int, Int>()
// Gets all children downloads // Gets all children downloads
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
for (c in children) { for (c in children) {
@ -69,7 +70,7 @@ class DownloadViewModel : ViewModel() {
} }
} }
val cached = withContext(Dispatchers.IO) { // wont fetch useless keys val cached = withContext(Dispatchers.IO) { // Won't fetch useless keys
totalDownloads.entries.filter { it.value > 0 }.mapNotNull { totalDownloads.entries.filter { it.value > 0 }.mapNotNull {
context.getKey<VideoDownloadHelper.DownloadHeaderCached>( context.getKey<VideoDownloadHelper.DownloadHeaderCached>(
DOWNLOAD_HEADER_CACHE, DOWNLOAD_HEADER_CACHE,
@ -79,7 +80,7 @@ class DownloadViewModel : ViewModel() {
} }
val visual = withContext(Dispatchers.IO) { val visual = withContext(Dispatchers.IO) {
cached.mapNotNull { // TODO FIX cached.mapNotNull {
val downloads = totalDownloads[it.id] ?: 0 val downloads = totalDownloads[it.id] ?: 0
val bytes = totalBytesUsedByChild[it.id] ?: 0 val bytes = totalBytesUsedByChild[it.id] ?: 0
val currentBytes = currentBytesUsedByChild[it.id] ?: 0 val currentBytes = currentBytesUsedByChild[it.id] ?: 0
@ -91,32 +92,37 @@ class DownloadViewModel : ViewModel() {
getFolderName(it.id.toString(), it.id.toString()) getFolderName(it.id.toString(), it.id.toString())
) )
VisualDownloadHeaderCached( VisualDownloadHeaderCached(
0, currentBytes = currentBytes,
downloads, totalBytes = bytes,
bytes, data = it,
currentBytes, child = movieEpisode,
it, currentOngoingDownloads = 0,
movieEpisode totalDownloads = downloads,
) )
}.sortedBy { }.sortedBy {
(it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0)
} // episode sorting by episode, lowest to highest } // Episode sorting by episode, lowest to highest
} }
// Only update list if different from the previous one to prevent duplicate initialization
if (visual != previousVisual) {
previousVisual = visual
try { try {
val stat = StatFs(Environment.getExternalStorageDirectory().path) val stat = StatFs(Environment.getExternalStorageDirectory().path)
val localBytesAvailable = stat.availableBytes
val localBytesAvailable = stat.availableBytes//stat.blockSizeLong * stat.blockCountLong
val localTotalBytes = stat.blockSizeLong * stat.blockCountLong val localTotalBytes = stat.blockSizeLong * stat.blockCountLong
val localDownloadedBytes = visual.sumOf { it.totalBytes } val localDownloadedBytes = visual.sumOf { it.totalBytes }
_usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes)
_availableBytes.postValue(localBytesAvailable) _availableBytes.postValue(localBytesAvailable)
_downloadBytes.postValue(localDownloadedBytes) _downloadBytes.postValue(localDownloadedBytes)
} catch (t : Throwable) { } catch (t: Throwable) {
_downloadBytes.postValue(0) _downloadBytes.postValue(0)
logError(t) logError(t)
} }
_headerCards.postValue(visual) _headerCards.postValue(visual)
} }
}
} }

View file

@ -192,15 +192,15 @@ class EpisodeAdapter(
downloadButton.isVisible = hasDownloadSupport downloadButton.isVisible = hasDownloadSupport
downloadButton.setDefaultClickListener( downloadButton.setDefaultClickListener(
VideoDownloadHelper.DownloadEpisodeCached( VideoDownloadHelper.DownloadEpisodeCached(
card.name, name = card.name,
card.poster, poster = card.poster,
card.episode, episode = card.episode,
card.season, season = card.season,
card.id, id = card.id,
card.parentId, parentId = card.parentId,
card.rating, rating = card.rating,
card.description, description = card.description,
System.currentTimeMillis(), cacheTime = System.currentTimeMillis(),
), null ), null
) { ) {
when (it.action) { when (it.action) {
@ -343,15 +343,15 @@ class EpisodeAdapter(
downloadButton.isVisible = hasDownloadSupport downloadButton.isVisible = hasDownloadSupport
downloadButton.setDefaultClickListener( downloadButton.setDefaultClickListener(
VideoDownloadHelper.DownloadEpisodeCached( VideoDownloadHelper.DownloadEpisodeCached(
card.name, name = card.name,
card.poster, poster = card.poster,
card.episode, episode = card.episode,
card.season, season = card.season,
card.id, id = card.id,
card.parentId, parentId = card.parentId,
card.rating, rating = card.rating,
card.description, description = card.description,
System.currentTimeMillis(), cacheTime = System.currentTimeMillis(),
), null ), null
) { ) {
when (it.action) { when (it.action) {

View file

@ -185,8 +185,6 @@ open class ResultFragmentPhone : FullScreenPlayer() {
} }
binding?.resultFullscreenHolder?.isVisible = !isSuccess && isFullScreenPlayer binding?.resultFullscreenHolder?.isVisible = !isSuccess && isFullScreenPlayer
} }
//player_view?.apply { //player_view?.apply {
//alpha = 0.0f //alpha = 0.0f
//ObjectAnimator.ofFloat(player_view, "alpha", 1f).apply { //ObjectAnimator.ofFloat(player_view, "alpha", 1f).apply {
@ -200,9 +198,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
// fillAfter = true // fillAfter = true
//} //}
//startAnimation(fadeIn) //startAnimation(fadeIn)
// } //}
} }
private fun setTrailers(trailers: List<ExtractorLink>?) { private fun setTrailers(trailers: List<ExtractorLink>?) {
@ -630,15 +626,15 @@ open class ResultFragmentPhone : FullScreenPlayer() {
} }
downloadButton.setDefaultClickListener( downloadButton.setDefaultClickListener(
VideoDownloadHelper.DownloadEpisodeCached( VideoDownloadHelper.DownloadEpisodeCached(
ep.name, name = ep.name,
ep.poster, poster = ep.poster,
0, episode = 0,
null, season = null,
ep.id, id = ep.id,
ep.id, parentId = ep.id,
null, rating = null,
null, description = null,
System.currentTimeMillis(), cacheTime = System.currentTimeMillis(),
), ),
null null
) { click -> ) { click ->

View file

@ -705,13 +705,13 @@ class ResultViewModel2 : ViewModel() {
DOWNLOAD_HEADER_CACHE, DOWNLOAD_HEADER_CACHE,
parentId.toString(), parentId.toString(),
VideoDownloadHelper.DownloadHeaderCached( VideoDownloadHelper.DownloadHeaderCached(
apiName, apiName = apiName,
url, url = url,
currentType, type = currentType,
currentHeaderName, name = currentHeaderName,
currentPoster, poster = currentPoster,
parentId, id = parentId,
System.currentTimeMillis(), cacheTime = System.currentTimeMillis(),
) )
) )
@ -722,15 +722,15 @@ class ResultViewModel2 : ViewModel() {
), // 3 deep folder for faster acess ), // 3 deep folder for faster acess
episode.id.toString(), episode.id.toString(),
VideoDownloadHelper.DownloadEpisodeCached( VideoDownloadHelper.DownloadEpisodeCached(
episode.name, name = episode.name,
episode.poster, poster = episode.poster,
episode.episode, episode = episode.episode,
episode.season, season = episode.season,
episode.id, id = episode.id,
parentId, parentId = parentId,
episode.rating, rating = episode.rating,
episode.description, description = episode.description,
System.currentTimeMillis(), cacheTime = System.currentTimeMillis(),
) )
) )
@ -2776,13 +2776,13 @@ class ResultViewModel2 : ViewModel() {
DOWNLOAD_HEADER_CACHE, DOWNLOAD_HEADER_CACHE,
mainId.toString(), mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached( VideoDownloadHelper.DownloadHeaderCached(
apiName, apiName = apiName,
validUrl, url = validUrl,
loadResponse.type, type = loadResponse.type,
loadResponse.name, name = loadResponse.name,
loadResponse.posterUrl, poster = loadResponse.posterUrl,
mainId, id = mainId,
System.currentTimeMillis(), cacheTime = System.currentTimeMillis(),
) )
) )
if (loadTrailers) if (loadTrailers)

View file

@ -25,7 +25,7 @@ object SearchHelper {
SEARCH_ACTION_PLAY_FILE -> { SEARCH_ACTION_PLAY_FILE -> {
if (card is DataStoreHelper.ResumeWatchingResult) { if (card is DataStoreHelper.ResumeWatchingResult) {
val id = card.id val id = card.id
if(id == null) { if (id == null) {
showToast(R.string.error_invalid_id, Toast.LENGTH_SHORT) showToast(R.string.error_invalid_id, Toast.LENGTH_SHORT)
} else { } else {
if (card.isFromDownload) { if (card.isFromDownload) {
@ -33,15 +33,15 @@ object SearchHelper {
DownloadClickEvent( DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE, DOWNLOAD_ACTION_PLAY_FILE,
VideoDownloadHelper.DownloadEpisodeCached( VideoDownloadHelper.DownloadEpisodeCached(
card.name, name = card.name,
card.posterUrl, poster = card.posterUrl,
card.episode ?: 0, episode = card.episode ?: 0,
card.season, season = card.season,
id, id = id,
card.parentId ?: return, parentId = card.parentId ?: return,
null, rating = null,
null, description = null,
System.currentTimeMillis() cacheTime = System.currentTimeMillis(),
) )
) )
) )

View file

@ -3,17 +3,21 @@ package com.lagradost.cloudstream3.utils
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
object VideoDownloadHelper { object VideoDownloadHelper {
abstract class DownloadCached(
@JsonProperty("id") open val id: Int,
)
data class DownloadEpisodeCached( data class DownloadEpisodeCached(
@JsonProperty("name") val name: String?, @JsonProperty("name") val name: String?,
@JsonProperty("poster") val poster: String?, @JsonProperty("poster") val poster: String?,
@JsonProperty("episode") val episode: Int, @JsonProperty("episode") val episode: Int,
@JsonProperty("season") val season: Int?, @JsonProperty("season") val season: Int?,
@JsonProperty("id") val id: Int,
@JsonProperty("parentId") val parentId: Int, @JsonProperty("parentId") val parentId: Int,
@JsonProperty("rating") val rating: Int?, @JsonProperty("rating") val rating: Int?,
@JsonProperty("description") val description: String?, @JsonProperty("description") val description: String?,
@JsonProperty("cacheTime") val cacheTime: Long, @JsonProperty("cacheTime") val cacheTime: Long,
) override val id: Int,
): DownloadCached(id)
data class DownloadHeaderCached( data class DownloadHeaderCached(
@JsonProperty("apiName") val apiName: String, @JsonProperty("apiName") val apiName: String,
@ -21,9 +25,9 @@ object VideoDownloadHelper {
@JsonProperty("type") val type: TvType, @JsonProperty("type") val type: TvType,
@JsonProperty("name") val name: String, @JsonProperty("name") val name: String,
@JsonProperty("poster") val poster: String?, @JsonProperty("poster") val poster: String?,
@JsonProperty("id") val id: Int,
@JsonProperty("cacheTime") val cacheTime: Long, @JsonProperty("cacheTime") val cacheTime: Long,
) override val id: Int,
): DownloadCached(id)
data class ResumeWatching( data class ResumeWatching(
@JsonProperty("parentId") val parentId: Int, @JsonProperty("parentId") val parentId: Int,

View file

@ -59,12 +59,12 @@
<ImageView <ImageView
android:id="@+id/download_header_goto_child" android:id="@+id/download_header_goto_child"
android:layout_width="50dp" android:layout_width="@dimen/download_size"
android:layout_height="match_parent" android:layout_height="@dimen/download_size"
android:layout_gravity="center_vertical|end" android:layout_gravity="center_vertical|end"
android:layout_marginStart="-50dp" android:layout_marginStart="-50dp"
android:contentDescription="@string/download" android:contentDescription="@string/download"
android:padding="50dp" android:padding="10dp"
android:src="@drawable/ic_baseline_keyboard_arrow_right_24" /> android:src="@drawable/ic_baseline_keyboard_arrow_right_24" />
<com.lagradost.cloudstream3.ui.download.button.PieFetchButton <com.lagradost.cloudstream3.ui.download.button.PieFetchButton