diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 07a82583..91da2ed0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -622,7 +622,7 @@ abstract class MainAPI { /**Used for testing and can be used to disable the providers if WebView is not available*/ open val usesWebView = false - /** Determines which plugin a given provider is from */ + /** Determines which plugin a given provider is from. This is the full path to the plugin. */ var sourcePlugin: String? = null open val hasMainPage = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 6b7dc90b..7f08af92 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -67,6 +67,7 @@ abstract class Plugin { * This will contain your resources if you specified requiresResources in gradle */ var resources: Resources? = null + /** Full file path to the plugin. */ var __filename: String? = null /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index a30af11c..a5631500 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -18,7 +18,6 @@ import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider @@ -518,7 +517,7 @@ object PluginManager { return true } - pluginInstance.__filename = fileName + pluginInstance.__filename = file.absolutePath if (manifest.requiresResources) { Log.d(TAG, "Loading resources for ${data.internalName}") // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt new file mode 100644 index 00000000..8f496b3c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -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(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() { + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 10ce67a7..880d5f6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.download -import android.app.Activity import android.content.DialogInterface import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -22,7 +21,6 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager object DownloadButtonSetup { fun handleDownloadClick(click: DownloadClickEvent) { val id = click.data.id - if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return when (click.action) { DOWNLOAD_ACTION_DELETE_FILE -> { activity?.let { ctx -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt deleted file mode 100644 index 1d7b5a83..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ /dev/null @@ -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, - private val clickCallback: (DownloadClickEvent) -> Unit, -) : RecyclerView.Adapter() { - - 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)) - } - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index f54c8698..7734cb08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick @@ -40,7 +39,8 @@ class DownloadChildFragment : Fragment() { super.onDestroyView() } - var binding: FragmentChildDownloadsBinding? = null + private var binding: FragmentChildDownloadsBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -48,7 +48,7 @@ class DownloadChildFragment : Fragment() { ): View { val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) binding = localBinding - return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false) + return localBinding.root } private fun updateList(folder: String) = main { @@ -60,7 +60,11 @@ class DownloadChildFragment : Fragment() { }.mapNotNull { val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id) ?: 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 } if (eps.isEmpty()) { @@ -68,9 +72,7 @@ class DownloadChildFragment : Fragment() { return@main } - (binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList = - eps - binding?.downloadChildList?.adapter?.notifyDataSetChanged() + (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(eps) } } @@ -98,31 +100,39 @@ class DownloadChildFragment : Fragment() { setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = - DownloadChildAdapter( - ArrayList(), - ) { click -> - handleDownloadClick(click) + val adapter = DownloadAdapter( + {}, + { downloadClickEvent -> + handleDownloadClick(downloadClickEvent) + 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 -> - val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList + val list = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.currentList if (list != null) { if (list.any { it.data.id == id }) { updateList(folder) } } } - 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) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 31790b0f..de2d4f3c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -10,14 +10,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout +import android.widget.TextView import android.widget.Toast +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R 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.navigate import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV -import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI - const val DOWNLOAD_NAVIGATE_TO = "downloadpage" class DownloadFragment : Fragment() { @@ -63,33 +62,30 @@ class DownloadFragment : Fragment() { private fun setList(list: List) { main { - (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list - binding?.downloadList?.adapter?.notifyDataSetChanged() + (binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(list) } } override fun onDestroyView() { - if (downloadDeleteEventListener != null) { - VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!! - downloadDeleteEventListener = null + downloadDeleteEventListener?.let { + VideoDownloadManager.downloadDeleteEvent -= it } + downloadDeleteEventListener = null binding = null super.onDestroyView() } - var binding: FragmentDownloadsBinding? = null + private var binding: FragmentDownloadsBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - downloadsViewModel = - ViewModelProvider(this)[DownloadViewModel::class.java] - + ): View { + downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false) binding = localBinding - return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false) + return localBinding.root } private var downloadDeleteEventListener: ((Int) -> Unit)? = null @@ -97,7 +93,6 @@ class DownloadFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) hideKeyboard() - binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() observe(downloadsViewModel.noDownloadsText) { @@ -108,176 +103,148 @@ class DownloadFragment : Fragment() { binding?.downloadLoading?.isVisible = false } observe(downloadsViewModel.availableBytes) { - binding?.downloadFreeTxt?.text = - getString(R.string.storage_size_format).format( - getString(R.string.free_storage), - formatShortFileSize(view.context, it) - ) - binding?.downloadFree?.setLayoutWidth(it) + updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree) } observe(downloadsViewModel.usedBytes) { - binding?.apply { - downloadUsedTxt.text = - getString(R.string.storage_size_format).format( - getString(R.string.used_storage), - formatShortFileSize(view.context, it) - ) - downloadUsed.setLayoutWidth(it) - downloadStorageAppbar.isVisible = it > 0 - } + updateStorageInfo(view.context, it, R.string.used_storage, binding?.downloadUsedTxt, binding?.downloadUsed) + binding?.downloadStorageAppbar?.isVisible = it > 0 } observe(downloadsViewModel.downloadBytes) { - binding?.apply { - downloadAppTxt.text = - getString(R.string.storage_size_format).format( - getString(R.string.app_storage), - formatShortFileSize(view.context, it) - ) - downloadApp.setLayoutWidth(it) - } + updateStorageInfo(view.context, it, R.string.app_storage, binding?.downloadAppTxt, binding?.downloadApp) } - val adapter: RecyclerView.Adapter = - DownloadHeaderAdapter( - ArrayList(), - { click -> - when (click.action) { - 0 -> { - if (click.data.type.isMovieType()) { - //wont be called - } else { - val folder = DataStore.getFolderName( - DOWNLOAD_EPISODE_CACHE, - click.data.id.toString() - ) - activity?.navigate( - R.id.action_navigation_downloads_to_navigation_download_child, - DownloadChildFragment.newInstance(click.data.name, folder) - ) - } - } - - 1 -> { - (activity as AppCompatActivity?)?.loadResult( - click.data.url, - click.data.apiName - ) - } - } - - }, - { 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 -> - val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList - if (list != null) { - if (list.any { it.data.id == id }) { - context?.let { ctx -> - setList(ArrayList()) - downloadsViewModel.updateList(ctx) - } + val adapter = DownloadAdapter( + { click -> + handleItemClick(click) + }, + { downloadClickEvent -> + handleDownloadClick(downloadClickEvent) + if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + setUpDownloadDeleteListener() } } - } - - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + ) binding?.downloadList?.apply { + setHasFixedSize(true) + setItemViewCacheSize(20) this.adapter = adapter setLinearListLayout( isHorizontal = false, nextRight = FOCUS_SELF, nextUp = FOCUS_SELF, - nextDown = FOCUS_SELF + nextDown = FOCUS_SELF, ) - //layoutManager = GridLayoutManager(context, 1) } - // Should be visible in emulator layout - binding?.downloadStreamButton?.isGone = isLayout(TV) - binding?.downloadStreamButton?.setOnClickListener { - val dialog = - Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) - - val binding = StreamInputBinding.inflate(dialog.layoutInflater) - - dialog.setContentView(binding.root) - - dialog.show() - - // If user has clicked the switch do not interfere - var preventAutoSwitching = false - binding.hlsSwitch.setOnClickListener { - preventAutoSwitching = true - } - - fun activateSwitchOnHls(text: String?) { - binding.hlsSwitch.isChecked = normalSafeApiCall { - URI(text).path?.substringAfterLast(".")?.contains("m3u") - } == true - } - - binding.streamReferer.doOnTextChanged { text, _, _, _ -> - if (!preventAutoSwitching) - activateSwitchOnHls(text?.toString()) - } - - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt( - 0 - )?.text?.toString()?.let { copy -> - val fixedText = copy.trim() - binding.streamUrl.setText(fixedText) - activateSwitchOnHls(fixedText) - } - - binding.applyBtt.setOnClickListener { - val url = binding.streamUrl.text?.toString() - if (url.isNullOrEmpty()) { - showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) - } else { - val referer = binding.streamReferer.text?.toString() - - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - LinkGenerator( - listOf(BasicLink(url)), - extract = true, - referer = referer, - isM3u8 = binding.hlsSwitch.isChecked - ) - ) - ) - - dialog.dismissSafe(activity) - } - } - - binding.cancelBtt.setOnClickListener { - dialog.dismissSafe(activity) - } + binding?.downloadStreamButton?.apply { + isGone = isLayout(TV) + setOnClickListener { showStreamInputDialog(it.context) } } + 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 - } + handleScroll(scrollY - oldScrollY) } } downloadsViewModel.updateList(requireContext()) - fixPaddingStatusbar(binding?.downloadRoot) } + + private fun handleItemClick(click: DownloadHeaderClickEvent) { + when (click.action) { + 0 -> { + if (!click.data.type.isMovieType()) { + val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) + activity?.navigate( + R.id.action_navigation_downloads_to_navigation_download_child, + DownloadChildFragment.newInstance(click.data.name, folder) + ) + } + } + 1 -> { + (activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName) + } + } + } + + private fun setUpDownloadDeleteListener() { + downloadDeleteEventListener = { id -> + val list = (binding?.downloadList?.adapter as? DownloadAdapter)?.currentList + if (list?.any { it.data.id == id } == true) { + context?.let { downloadsViewModel.updateList(it) } + } + } + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateStorageInfo( + context: Context, + bytes: Long, + @StringRes stringRes: Int, + 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) + dialog.setContentView(binding.root) + dialog.show() + + var preventAutoSwitching = false + binding.hlsSwitch.setOnClickListener { preventAutoSwitching = true } + + binding.streamReferer.doOnTextChanged { text, _, _, _ -> + if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding) + } + + (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(0)?.text?.toString()?.let { copy -> + val fixedText = copy.trim() + binding.streamUrl.setText(fixedText) + activateSwitchOnHls(fixedText, binding) + } + + binding.applyBtt.setOnClickListener { + val url = binding.streamUrl.text?.toString() + if (url.isNullOrEmpty()) { + showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) + } else { + val referer = binding.streamReferer.text?.toString() + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + LinkGenerator( + listOf(BasicLink(url)), + extract = true, + referer = referer, + isM3u8 = binding.hlsSwitch.isChecked + ) + ) + ) + dialog.dismissSafe(activity) + } + } + + binding.cancelBtt.setOnClickListener { + dialog.dismissSafe(activity) + } + } + + 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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt deleted file mode 100644 index 65a6441f..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt +++ /dev/null @@ -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, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val movieClickCallback: (DownloadClickEvent) -> Unit, -) : RecyclerView.Adapter() { - - 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)) - } - } - } - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 3a74a715..380430e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -39,6 +39,8 @@ class DownloadViewModel : ViewModel() { val availableBytes: LiveData = _availableBytes val downloadBytes: LiveData = _downloadBytes + private var previousVisual: List? = null + fun updateList(context: Context) = viewModelScope.launchSafe { val children = withContext(Dispatchers.IO) { val headers = context.getKeys(DOWNLOAD_EPISODE_CACHE) @@ -53,7 +55,6 @@ class DownloadViewModel : ViewModel() { // parentId : downloadsCount val totalDownloads = HashMap() - // Gets all children downloads withContext(Dispatchers.IO) { 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 { context.getKey( DOWNLOAD_HEADER_CACHE, @@ -79,7 +80,7 @@ class DownloadViewModel : ViewModel() { } val visual = withContext(Dispatchers.IO) { - cached.mapNotNull { // TODO FIX + cached.mapNotNull { val downloads = totalDownloads[it.id] ?: 0 val bytes = totalBytesUsedByChild[it.id] ?: 0 val currentBytes = currentBytesUsedByChild[it.id] ?: 0 @@ -91,32 +92,37 @@ class DownloadViewModel : ViewModel() { getFolderName(it.id.toString(), it.id.toString()) ) VisualDownloadHeaderCached( - 0, - downloads, - bytes, - currentBytes, - it, - movieEpisode + currentBytes = currentBytes, + totalBytes = bytes, + data = it, + child = movieEpisode, + currentOngoingDownloads = 0, + totalDownloads = downloads, ) }.sortedBy { (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) - } // episode sorting by episode, lowest to highest - } - try { - val stat = StatFs(Environment.getExternalStorageDirectory().path) - - val localBytesAvailable = stat.availableBytes//stat.blockSizeLong * stat.blockCountLong - val localTotalBytes = stat.blockSizeLong * stat.blockCountLong - val localDownloadedBytes = visual.sumOf { it.totalBytes } - - _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) - _availableBytes.postValue(localBytesAvailable) - _downloadBytes.postValue(localDownloadedBytes) - } catch (t : Throwable) { - _downloadBytes.postValue(0) - logError(t) + } // Episode sorting by episode, lowest to highest } - _headerCards.postValue(visual) + // Only update list if different from the previous one to prevent duplicate initialization + if (visual != previousVisual) { + previousVisual = visual + + try { + val stat = StatFs(Environment.getExternalStorageDirectory().path) + val localBytesAvailable = stat.availableBytes + val localTotalBytes = stat.blockSizeLong * stat.blockCountLong + val localDownloadedBytes = visual.sumOf { it.totalBytes } + + _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) + _availableBytes.postValue(localBytesAvailable) + _downloadBytes.postValue(localDownloadedBytes) + } catch (t: Throwable) { + _downloadBytes.postValue(0) + logError(t) + } + + _headerCards.postValue(visual) + } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index e4fd0559..62b1fdd1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -192,15 +192,15 @@ class EpisodeAdapter( downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.poster, - card.episode, - card.season, - card.id, - card.parentId, - card.rating, - card.description, - System.currentTimeMillis(), + name = card.name, + poster = card.poster, + episode = card.episode, + season = card.season, + id = card.id, + parentId = card.parentId, + rating = card.rating, + description = card.description, + cacheTime = System.currentTimeMillis(), ), null ) { when (it.action) { @@ -343,15 +343,15 @@ class EpisodeAdapter( downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.poster, - card.episode, - card.season, - card.id, - card.parentId, - card.rating, - card.description, - System.currentTimeMillis(), + name = card.name, + poster = card.poster, + episode = card.episode, + season = card.season, + id = card.id, + parentId = card.parentId, + rating = card.rating, + description = card.description, + cacheTime = System.currentTimeMillis(), ), null ) { when (it.action) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index fb5160a7..e185e75d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -185,8 +185,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } binding?.resultFullscreenHolder?.isVisible = !isSuccess && isFullScreenPlayer } - - //player_view?.apply { //alpha = 0.0f //ObjectAnimator.ofFloat(player_view, "alpha", 1f).apply { @@ -200,9 +198,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { // fillAfter = true //} //startAnimation(fadeIn) - // } - - + //} } private fun setTrailers(trailers: List?) { @@ -630,15 +626,15 @@ open class ResultFragmentPhone : FullScreenPlayer() { } downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - ep.name, - ep.poster, - 0, - null, - ep.id, - ep.id, - null, - null, - System.currentTimeMillis(), + name = ep.name, + poster = ep.poster, + episode = 0, + season = null, + id = ep.id, + parentId = ep.id, + rating = null, + description = null, + cacheTime = System.currentTimeMillis(), ), null ) { click -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 4285feb1..ac6527de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -705,13 +705,13 @@ class ResultViewModel2 : ViewModel() { DOWNLOAD_HEADER_CACHE, parentId.toString(), VideoDownloadHelper.DownloadHeaderCached( - apiName, - url, - currentType, - currentHeaderName, - currentPoster, - parentId, - System.currentTimeMillis(), + apiName = apiName, + url = url, + type = currentType, + name = currentHeaderName, + poster = currentPoster, + id = parentId, + cacheTime = System.currentTimeMillis(), ) ) @@ -722,15 +722,15 @@ class ResultViewModel2 : ViewModel() { ), // 3 deep folder for faster acess episode.id.toString(), VideoDownloadHelper.DownloadEpisodeCached( - episode.name, - episode.poster, - episode.episode, - episode.season, - episode.id, - parentId, - episode.rating, - episode.description, - System.currentTimeMillis(), + name = episode.name, + poster = episode.poster, + episode = episode.episode, + season = episode.season, + id = episode.id, + parentId = parentId, + rating = episode.rating, + description = episode.description, + cacheTime = System.currentTimeMillis(), ) ) @@ -2776,13 +2776,13 @@ class ResultViewModel2 : ViewModel() { DOWNLOAD_HEADER_CACHE, mainId.toString(), VideoDownloadHelper.DownloadHeaderCached( - apiName, - validUrl, - loadResponse.type, - loadResponse.name, - loadResponse.posterUrl, - mainId, - System.currentTimeMillis(), + apiName = apiName, + url = validUrl, + type = loadResponse.type, + name = loadResponse.name, + poster = loadResponse.posterUrl, + id = mainId, + cacheTime = System.currentTimeMillis(), ) ) if (loadTrailers) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index 5b943105..66423982 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -25,7 +25,7 @@ object SearchHelper { SEARCH_ACTION_PLAY_FILE -> { if (card is DataStoreHelper.ResumeWatchingResult) { val id = card.id - if(id == null) { + if (id == null) { showToast(R.string.error_invalid_id, Toast.LENGTH_SHORT) } else { if (card.isFromDownload) { @@ -33,15 +33,15 @@ object SearchHelper { DownloadClickEvent( DOWNLOAD_ACTION_PLAY_FILE, VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.posterUrl, - card.episode ?: 0, - card.season, - id, - card.parentId ?: return, - null, - null, - System.currentTimeMillis() + name = card.name, + poster = card.posterUrl, + episode = card.episode ?: 0, + season = card.season, + id = id, + parentId = card.parentId ?: return, + rating = null, + description = null, + cacheTime = System.currentTimeMillis(), ) ) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index 83480542..023ecb4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -2,26 +2,31 @@ package com.lagradost.cloudstream3.ui.settings.testing import android.app.AlertDialog import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getStackTracePretty +import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.TestingUtils +import java.io.File class TestResultAdapter(override val items: MutableList>) : AppUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProviderTestViewHolder( - ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent,false) + ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) //LayoutInflater.from(parent.context) // .inflate(R.layout.provider_test_item, parent, false), ) @@ -36,7 +41,8 @@ class TestResultAdapter(override val items: MutableList } + + api.sourcePlugin?.let { path -> + val pluginFile = File(path) + // Cannot delete a deleted plugin + if (!pluginFile.exists()) return@let + + builder.setNegativeButton(R.string.delete_plugin) { _, _ -> + ioSafe { + val success = PluginManager.deletePlugin(pluginFile) + + runOnMainThread { + if (success) { + showToast(R.string.plugin_deleted, Toast.LENGTH_SHORT) + } else { + showToast(R.string.error, Toast.LENGTH_SHORT) + } + } + } + } + } + + builder.show() } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 9e6f8a06..818f1fd7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -95,7 +95,7 @@ class TestViewModel : ViewModel() { providers.clear() updateProgress() - TestingUtils.getDeferredProviderTests(scope ?: return, apis, ::println) { api, result -> + TestingUtils.getDeferredProviderTests(scope ?: return, apis) { api, result -> addProvider(api, result) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 1302453a..12b8837a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1015,7 +1015,7 @@ abstract class ExtractorApi { abstract val mainUrl: String abstract val requiresReferer: Boolean - /** Determines which plugin a given extractor is from */ + /** Determines which plugin a given provider is from. This is the full path to the plugin. */ var sourcePlugin: String? = null //suspend fun getSafeUrl(url: String, referer: String? = null): List? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index dd973538..5e2b2bc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -13,16 +13,55 @@ object TestingUtils { } } - class TestResultSearch(val results: List) : TestResult(true) - class TestResultLoad(val extractorData: String) : TestResult(true) + class Logger { + enum class LogLevel { + Normal, + Warning, + Error; + } - class TestResultProvider(success: Boolean, val log: String, val exception: Throwable?) : + data class Message(val level: LogLevel, val message: String) { + override fun toString(): String { + val level = when (this.level) { + LogLevel.Normal -> "" + LogLevel.Warning -> "Warning: " + LogLevel.Error -> "Error: " + } + return "$level$message" + } + } + + private val messageLog = mutableListOf() + + fun getRawLog(): List = messageLog + + fun log(message: String) { + messageLog.add(Message(LogLevel.Normal, message)) + } + + fun warn(message: String) { + messageLog.add(Message(LogLevel.Warning, message)) + } + + fun error(message: String) { + messageLog.add(Message(LogLevel.Error, message)) + } + } + + class TestResultList(val results: List) : TestResult(true) + class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true) + + class TestResultProvider( + success: Boolean, + val log: List, + val exception: Throwable? + ) : TestResult(success) @Throws(AssertionError::class, CancellationException::class) suspend fun testHomepage( api: MainAPI, - logger: (String) -> Unit + logger: Logger ): TestResult { if (api.hasMainPage) { try { @@ -31,22 +70,33 @@ object TestingUtils { api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages)) when { homepage == null -> { - logger.invoke("Homepage provider ${api.name} did not correctly load homepage!") + logger.error("Provider ${api.name} did not correctly load homepage!") } + homepage.items.isEmpty() -> { - logger.invoke("Homepage provider ${api.name} does not contain any items!") + logger.warn("Provider ${api.name} does not contain any homepage rows!") } + homepage.items.any { it.list.isEmpty() } -> { - logger.invoke("Homepage provider ${api.name} does not have any items on result!") + logger.warn("Provider ${api.name} does not have any items in a homepage row!") } } + val homePageList = homepage?.items?.flatMap { it.list } ?: emptyList() + return TestResultList(homePageList) } catch (e: Throwable) { - if (e is NotImplementedError) { - Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") - } else if (e is CancellationException) { - throw e + when (e) { + is NotImplementedError -> { + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") + } + + is CancellationException -> { + throw e + } + + else -> { + e.message?.let { logger.warn("Exception thrown when loading homepage: \"$it\"") } + } } - logError(e) } } return TestResult.Pass @@ -54,11 +104,13 @@ object TestingUtils { @Throws(AssertionError::class, CancellationException::class) private suspend fun testSearch( - api: MainAPI + api: MainAPI, + testQueries: List, + logger: Logger, ): TestResult { - val searchQueries = listOf("over", "iron", "guy") - val searchResults = searchQueries.firstNotNullOfOrNull { query -> + val searchResults = testQueries.firstNotNullOfOrNull { query -> try { + logger.log("Searching for: $query") api.search(query).takeIf { !it.isNullOrEmpty() } } catch (e: Throwable) { if (e is NotImplementedError) { @@ -72,12 +124,11 @@ object TestingUtils { } return if (searchResults.isNullOrEmpty()) { - Assert.fail("Api ${api.name} did not return any valid search responses") + Assert.fail("Api ${api.name} did not return any search responses") TestResult.Fail // Should not be reached } else { - TestResultSearch(searchResults) + TestResultList(searchResults) } - } @@ -85,31 +136,27 @@ object TestingUtils { private suspend fun testLoad( api: MainAPI, result: SearchResponse, - logger: (String) -> Unit + logger: Logger ): TestResult { try { - Assert.assertEquals( - "Invalid apiName on SearchResponse on ${api.name}", - result.apiName, - api.name - ) + if (result.apiName != api.name) { + logger.warn("Wrong apiName on SearchResponse: ${api.name} != ${result.apiName}") + } val loadResponse = api.load(result.url) if (loadResponse == null) { - logger.invoke("Returned null loadResponse on ${result.url} on ${api.name}") + logger.error("Returned null loadResponse on ${result.url} on ${api.name}") return TestResult.Fail } - Assert.assertEquals( - "Invalid apiName on LoadResponse on ${api.name}", - loadResponse.apiName, - result.apiName - ) - Assert.assertTrue( - "Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}", - api.supportedTypes.contains(loadResponse.type) - ) + if (loadResponse.apiName != api.name) { + logger.warn("Wrong apiName on LoadResponse: ${api.name} != ${loadResponse.apiName}") + } + + if (!api.supportedTypes.contains(loadResponse.type)) { + logger.warn("Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}") + } val url = when (loadResponse) { is AnimeLoadResponse -> { @@ -117,39 +164,43 @@ object TestingUtils { loadResponse.episodes.keys.isEmpty() || loadResponse.episodes.keys.any { loadResponse.episodes[it].isNullOrEmpty() } if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") + logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } (loadResponse.episodes[loadResponse.episodes.keys.firstOrNull()])?.firstOrNull()?.data } + is MovieLoadResponse -> { val gotNoEpisodes = loadResponse.dataUrl.isBlank() if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no movie on ${loadResponse.url}") + logger.error("Api ${api.name} got no movie on ${loadResponse.url}") return TestResult.Fail } loadResponse.dataUrl } + is TvSeriesLoadResponse -> { val gotNoEpisodes = loadResponse.episodes.isEmpty() if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") + logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } loadResponse.episodes.firstOrNull()?.data } + is LiveStreamLoadResponse -> { loadResponse.dataUrl } + else -> { - logger.invoke("Unknown load response: ${loadResponse.javaClass.name}") + logger.error("Unknown load response: ${loadResponse.javaClass.name}") return TestResult.Fail } } ?: return TestResult.Fail - return TestResultLoad(url) + return TestResultLoad(url, loadResponse.type != TvType.CustomMedia) // val loadTest = testLoadResponse(api, load, logger) // if (loadTest is TestResultLoad) { @@ -174,7 +225,7 @@ object TestingUtils { private suspend fun testLinkLoading( api: MainAPI, url: String?, - logger: (String) -> Unit + logger: Logger ): TestResult { Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) if (url == null) return TestResult.Fail // Should never trigger @@ -182,7 +233,7 @@ object TestingUtils { var linksLoaded = 0 try { val success = api.loadLinks(url, false, {}) { link -> - logger.invoke("Video loaded: ${link.name}") + logger.log("Video loaded: ${link.name}") Assert.assertTrue( "Api ${api.name} returns link with invalid url ${link.url}", link.url.length > 4 @@ -190,7 +241,7 @@ object TestingUtils { linksLoaded++ } if (success) { - logger.invoke("Links loaded: $linksLoaded") + logger.log("Links loaded: $linksLoaded") return TestResult(linksLoaded > 0) } else { Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") @@ -200,8 +251,9 @@ object TestingUtils { is NotImplementedError -> { Assert.fail("Provider has not implemented loadLinks()") } + else -> { - logger.invoke("Failed link loading on ${api.name} using data: $url") + logger.error("Failed link loading on ${api.name} using data: $url") throw e } } @@ -212,53 +264,57 @@ object TestingUtils { fun getDeferredProviderTests( scope: CoroutineScope, providers: Array, - logger: (String) -> Unit, callback: (MainAPI, TestResultProvider) -> Unit ) { providers.forEach { api -> scope.launch { - var log = "" - fun addToLog(string: String) { - log += string + "\n" - logger.invoke(string) - } - fun getLog(): String { - return log.removeSuffix("\n") - } + val logger = Logger() val result = try { - addToLog("Trying ${api.name}") + logger.log("Trying ${api.name}") // Test Homepage - val homepage = testHomepage(api, logger).success - Assert.assertTrue("Homepage failed to load", homepage) + val homepage = testHomepage(api, logger) + Assert.assertTrue("Homepage failed to load", homepage.success) + val homePageList = (homepage as? TestResultList)?.results ?: emptyList() // Test Search Results - val searchResults = testSearch(api) + val searchQueries = + // Use the first 3 home page results as queries since they are guaranteed to exist + (homePageList.take(3).map { it.name } + + // If home page is sparse then use generic search queries + listOf("over", "iron", "guy")).take(3) + + val searchResults = testSearch(api, searchQueries, logger) Assert.assertTrue("Failed to get search results", searchResults.success) - searchResults as TestResultSearch + searchResults as TestResultList // Test Load and LoadLinks // Only try the first 3 search results to prevent spamming val success = searchResults.results.take(3).any { searchResponse -> - addToLog("Testing search result: ${searchResponse.url}") - val loadResponse = testLoad(api, searchResponse, ::addToLog) + logger.log("Testing search result: ${searchResponse.url}") + val loadResponse = testLoad(api, searchResponse, logger) if (loadResponse !is TestResultLoad) { false } else { - testLinkLoading(api, loadResponse.extractorData, ::addToLog).success + if (loadResponse.shouldLoadLinks) { + testLinkLoading(api, loadResponse.extractorData, logger).success + } else { + logger.log("Skipping link loading test") + true + } } } if (success) { - logger.invoke("Success ${api.name}") - TestResultProvider(true, getLog(), null) + logger.log("Success ${api.name}") + TestResultProvider(true, logger.getRawLog(), null) } else { - logger.invoke("Error ${api.name}") - TestResultProvider(false, getLog(), null) + logger.error("Link loading failed") + TestResultProvider(false, logger.getRawLog(), null) } } catch (e: Throwable) { - TestResultProvider(false, getLog(), e) + TestResultProvider(false, logger.getRawLog(), e) } callback.invoke(api, result) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt index d1614bc1..30f66f83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -3,17 +3,21 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.TvType object VideoDownloadHelper { + abstract class DownloadCached( + @JsonProperty("id") open val id: Int, + ) + data class DownloadEpisodeCached( @JsonProperty("name") val name: String?, @JsonProperty("poster") val poster: String?, @JsonProperty("episode") val episode: Int, @JsonProperty("season") val season: Int?, - @JsonProperty("id") val id: Int, @JsonProperty("parentId") val parentId: Int, @JsonProperty("rating") val rating: Int?, @JsonProperty("description") val description: String?, @JsonProperty("cacheTime") val cacheTime: Long, - ) + override val id: Int, + ): DownloadCached(id) data class DownloadHeaderCached( @JsonProperty("apiName") val apiName: String, @@ -21,9 +25,9 @@ object VideoDownloadHelper { @JsonProperty("type") val type: TvType, @JsonProperty("name") val name: String, @JsonProperty("poster") val poster: String?, - @JsonProperty("id") val id: Int, @JsonProperty("cacheTime") val cacheTime: Long, - ) + override val id: Int, + ): DownloadCached(id) data class ResumeWatching( @JsonProperty("parentId") val parentId: Int, diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 21f79ca6..a0b64ce3 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -59,12 +59,12 @@ #48E484 #ea596e + #FF9800 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b39006ad..f577d6e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -304,6 +304,7 @@ Start Failed Passed + Warning Resume -30 +30 @@ -609,6 +610,7 @@ plugins This will also delete all repository plugins Delete repository + Delete plugin Download the list of sites you want to use Downloaded: %d Disabled: %d