From 57828527aabf505e20c101b97fe02c3247441c3a Mon Sep 17 00:00:00 2001 From: LagradOst Date: Sat, 24 Jul 2021 17:13:21 +0200 Subject: [PATCH] download stuff --- .../com/lagradost/cloudstream3/UIHelper.kt | 51 +++++---- .../ui/download/DownloadChildAdapter.kt | 103 +++++++++++++++++- .../ui/download/DownloadChildFragment.kt | 77 ++++++++----- .../cloudstream3/utils/Coroutines.kt | 8 ++ .../utils/VideoDownloadManager.kt | 18 ++- app/src/main/res/drawable/circle_shape.xml | 11 ++ .../res/drawable/circular_progress_bar.xml | 18 +++ .../ic_baseline_delete_outline_24.xml | 5 + .../res/layout/download_child_episode.xml | 46 ++++++-- .../main/res/layout/search_result_grid.xml | 4 +- app/src/main/res/values/strings.xml | 5 + 11 files changed, 281 insertions(+), 65 deletions(-) create mode 100644 app/src/main/res/drawable/circle_shape.xml create mode 100644 app/src/main/res/drawable/circular_progress_bar.xml create mode 100644 app/src/main/res/drawable/ic_baseline_delete_outline_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt index 6b0ab221..ba15dd67 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt @@ -50,18 +50,22 @@ object UIHelper { val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density) fun Activity.checkWrite(): Boolean { - return (ContextCompat.checkSelfPermission(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE) + return (ContextCompat.checkSelfPermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED) } fun Activity.requestRW() { - ActivityCompat.requestPermissions(this, + ActivityCompat.requestPermissions( + this, arrayOf( Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE ), - 1337) + 1337 + ) } @ColorInt @@ -141,6 +145,7 @@ object UIHelper { ) } } + private var currentAudioFocusRequest: AudioFocusRequest? = null private var currentAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null var onAudioFocusEvent = Event() @@ -246,18 +251,19 @@ object UIHelper { // Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY window.decorView.systemUiVisibility = ( View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - // Set the content to appear under the system bars so that the - // content doesn't resize when the system bars hide and show. - or View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - // Hide the nav bar and status bar - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_FULLSCREEN + // Set the content to appear under the system bars so that the + // content doesn't resize when the system bars hide and show. + or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + // Hide the nav bar and status bar + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_FULLSCREEN // or View.SYSTEM_UI_FLAG_LOW_PROFILE ) // window.addFlags(View.KEEP_SCREEN_ON) } + fun FragmentActivity.popCurrentPage() { val currentFragment = supportFragmentManager.fragments.lastOrNull { it.isVisible @@ -363,10 +369,11 @@ object UIHelper { } + /**id, icon, stringRes */ @SuppressLint("RestrictedApi") - inline fun View.popupMenu( + fun View.popupMenu( items: List>, - noinline onMenuItemClick: MenuItem.() -> Unit, + onMenuItemClick: MenuItem.() -> Unit, ): PopupMenu { val ctw = ContextThemeWrapper(context, R.style.PopupMenu) val popup = PopupMenu(ctw, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0) @@ -386,9 +393,11 @@ object UIHelper { return popup } - inline fun View.popupMenuNoIcons( + /**id, stringRes */ + @SuppressLint("RestrictedApi") + fun View.popupMenuNoIcons( items: List>, - noinline onMenuItemClick: MenuItem.() -> Unit, + onMenuItemClick: MenuItem.() -> Unit, ): PopupMenu { val ctw = ContextThemeWrapper(context, R.style.PopupMenu) val popup = PopupMenu(ctw, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0) @@ -408,15 +417,17 @@ object UIHelper { return popup } - inline fun View.popupMenuNoIconsAndNoStringres( + /**id, string */ + @SuppressLint("RestrictedApi") + fun View.popupMenuNoIconsAndNoStringres( items: List>, - noinline onMenuItemClick: MenuItem.() -> Unit, + onMenuItemClick: MenuItem.() -> Unit, ): PopupMenu { val ctw = ContextThemeWrapper(context, R.style.PopupMenu) val popup = PopupMenu(ctw, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0) - items.forEach { (id, stringRes) -> - popup.menu.add(0, id, 0, stringRes) + items.forEach { (id, string) -> + popup.menu.add(0, id, 0, string) } (popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true) 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 index a64c381a..a645df7a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt @@ -1,19 +1,34 @@ package com.lagradost.cloudstream3.ui.download +import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.DecelerateInterpolator +import android.widget.ImageView +import android.widget.ProgressBar import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.widget.ContentLoadingProgressBar import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.UIHelper.popupMenuNoIcons +import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadState import kotlinx.android.synthetic.main.download_child_episode.view.* +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 + data class VisualDownloadChildCached( val currentBytes: Long, val totalBytes: Long, @@ -56,6 +71,8 @@ class DownloadChildAdapter( 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 @SuppressLint("SetTextI18n") fun bind(card: VisualDownloadChildCached) { @@ -73,13 +90,91 @@ class DownloadChildAdapter( title.text = d.name ?: "Episode ${d.episode}" //TODO FIX val totalMbString = "%.1f".format(card.totalBytes / 1000000f) - val currentMbString = "%.1f".format(card.currentBytes / 1000000f) - extraInfo.text = - "${currentMbString}MB / ${totalMbString}MB" + var lastState: VideoDownloadManager.DownloadType? = null + var currentBytes: Long = card.currentBytes + + fun changeDownloadImage(state: VideoDownloadManager.DownloadType) { + runOnMainThread { + val img = when (state) { + VideoDownloadManager.DownloadType.IsPaused -> R.drawable.ic_baseline_play_arrow_24 + VideoDownloadManager.DownloadType.IsDownloading -> R.drawable.netflix_pause + else -> R.drawable.ic_baseline_delete_outline_24 + } + downloadImage?.setImageResource(img) + } + } + + fun fixDownloadedBytes(setCurrentBytes: Long, animate : Boolean) { + currentBytes = setCurrentBytes + runOnMainThread { + val currentMbString = "%.1f".format(currentBytes / 1000000f) + + extraInfo?.text = + "${currentMbString}MB / ${totalMbString}MB" + + progressBarDownload?.let { bar -> + bar.max = (card.totalBytes / 1000).toInt() + + if(animate) { + val animation: ObjectAnimator = ObjectAnimator.ofInt( + bar, + "progress", + bar.progress, + (currentBytes / 1000).toInt() + ) + animation.duration = 500 + animation.setAutoCancel(true) + animation.interpolator = DecelerateInterpolator() + animation.start() + } else { + bar.progress = (currentBytes / 1000).toInt() + } + } + } + } + fixDownloadedBytes(card.currentBytes, false) + changeDownloadImage(getDownloadState(card.data.id)) + + VideoDownloadManager.downloadProgressEvent += { downloadData -> + if (card.data.id == downloadData.first) { + fixDownloadedBytes(downloadData.second, true) + } + } + + VideoDownloadManager.downloadStatusEvent += { downloadData -> + if (card.data.id == downloadData.first) { + if (lastState != downloadData.second) { // TO PREVENT WASTING UI TIME + lastState = downloadData.second + changeDownloadImage(downloadData.second) + } + } + } holder.setOnClickListener { - clickCallback.invoke(DownloadClickEvent(0, d)) + clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) + } + + progressBarDownload.setOnClickListener { + val list = arrayListOf( + Pair(DOWNLOAD_ACTION_DELETE_FILE, R.string.popup_delete_file), + ) + + // DON'T RESUME A DOWNLOADED FILE + if (lastState != VideoDownloadManager.DownloadType.IsDone && (currentBytes * 100 / card.totalBytes < 98)) { + list.add( + if (lastState == VideoDownloadManager.DownloadType.IsDownloading) + Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download) + else + Pair(DOWNLOAD_ACTION_RESUME_DOWNLOAD, R.string.popup_resume_download) + ) + } + + it.popupMenuNoIcons( + list + ) { + clickCallback.invoke(DownloadClickEvent(itemId, 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 19478747..a406fc41 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 @@ -10,10 +10,8 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.UIHelper.fixPaddingStatusbar -import com.lagradost.cloudstream3.ui.player.PlayerData import com.lagradost.cloudstream3.ui.player.PlayerFragment import com.lagradost.cloudstream3.ui.player.UriData -import com.lagradost.cloudstream3.ui.result.getRealPosition import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys @@ -84,32 +82,59 @@ class DownloadChildFragment : Fragment() { DownloadChildAdapter( ArrayList(), ) { click -> - if (click.action == 0) { // TODO PLAY - val info = - VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(requireContext(), click.data.id) - ?: return@DownloadChildAdapter + val id = click.data.id + when (click.action) { + DOWNLOAD_ACTION_DELETE_FILE -> { + context?.let { ctx -> + VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id) + } + updateList(folder) + } + DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> { + VideoDownloadManager.downloadEvent.invoke( + Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause) + ) + updateList(folder) + } + DOWNLOAD_ACTION_RESUME_DOWNLOAD -> { + context?.let { ctx -> + val pkg = VideoDownloadManager.getDownloadResumePackage(ctx, id) + if(pkg != null) { + VideoDownloadManager.downloadFromResume(ctx, pkg) + } else { + VideoDownloadManager.downloadEvent.invoke( + Pair(click.data.id, VideoDownloadManager.DownloadActionType.Resume) + ) + } + } + } + DOWNLOAD_ACTION_PLAY_FILE -> { + val info = + VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(requireContext(), click.data.id) + ?: return@DownloadChildAdapter - (requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction() - .setCustomAnimations( - R.anim.enter_anim, - R.anim.exit_anim, - R.anim.pop_enter, - R.anim.pop_exit - ) - .add( - R.id.homeRoot, - PlayerFragment.newInstance( - UriData( - info.path.toString(), - click.data.id, - name ?: "null", - click.data.episode, - click.data.season - ), - context?.getViewPos(click.data.id)?.position ?: 0 + (requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction() + .setCustomAnimations( + R.anim.enter_anim, + R.anim.exit_anim, + R.anim.pop_enter, + R.anim.pop_exit ) - ) - .commit() + .add( + R.id.homeRoot, + PlayerFragment.newInstance( + UriData( + info.path.toString(), + click.data.id, + name ?: "null", + click.data.episode, + click.data.season + ), + context?.getViewPos(click.data.id)?.position ?: 0 + ) + ) + .commit() + } } } download_child_list.adapter = adapter diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt index e1b055e8..29b13881 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -1,5 +1,7 @@ package com.lagradost.cloudstream3.utils +import android.os.Handler +import android.os.Looper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -11,4 +13,10 @@ object Coroutines { work() } } + fun runOnMainThread(work: (() -> Unit)) { + val mainHandler = Handler(Looper.getMainLooper()) + mainHandler.post { + work() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 8af29a58..9c3881be 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -166,6 +166,16 @@ object VideoDownloadManager { } } + /** Will return IsDone if not found or error */ + fun getDownloadState(id : Int) : DownloadType { + return try { + downloadStatus[id] ?: DownloadType.IsDone + } catch (e : Exception) { + e.printStackTrace() + DownloadType.IsDone + } + } + private val cachedBitmaps = hashMapOf() private fun Context.getImageBitmapFromUrl(url: String): Bitmap? { if (cachedBitmaps.containsKey(url)) { @@ -532,6 +542,7 @@ object VideoDownloadManager { try { downloadStatus[ep.id] = type downloadStatusEvent.invoke(Pair(ep.id, type)) + downloadProgressEvent.invoke(Pair(ep.id, bytesDownloaded)) } catch (e: Exception) { // IDK MIGHT ERROR } @@ -584,7 +595,7 @@ object VideoDownloadManager { count = connectionInputStream.read(buffer) if (count < 0) break bytesDownloaded += count - downloadProgressEvent.invoke(Pair(id, bytesDownloaded)) + // downloadProgressEvent.invoke(Pair(id, bytesDownloaded)) // Updates too much for any UI to keep up with while (isPaused) { sleep(100) if (isStopped) { @@ -621,6 +632,7 @@ object VideoDownloadManager { deleteFile() } else -> { + downloadProgressEvent.invoke(Pair(id, bytesDownloaded)) isDone = true updateNotification() SUCCESS_DOWNLOAD_DONE @@ -741,6 +753,10 @@ object VideoDownloadManager { downloadQueue.addLast(pkg) downloadCheck(context) if (setKey) saveQueue(context) + } else { + downloadEvent.invoke( + Pair(pkg.item.ep.id, DownloadActionType.Resume) + ) } } diff --git a/app/src/main/res/drawable/circle_shape.xml b/app/src/main/res/drawable/circle_shape.xml new file mode 100644 index 00000000..56b8e4ef --- /dev/null +++ b/app/src/main/res/drawable/circle_shape.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circular_progress_bar.xml b/app/src/main/res/drawable/circular_progress_bar.xml new file mode 100644 index 00000000..7496129d --- /dev/null +++ b/app/src/main/res/drawable/circular_progress_bar.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_delete_outline_24.xml b/app/src/main/res/drawable/ic_baseline_delete_outline_24.xml new file mode 100644 index 00000000..8f98cc53 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_delete_outline_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index 05015ad3..0cdb1473 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -25,6 +25,7 @@ - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_result_grid.xml b/app/src/main/res/layout/search_result_grid.xml index 767420ba..6334a259 100644 --- a/app/src/main/res/layout/search_result_grid.xml +++ b/app/src/main/res/layout/search_result_grid.xml @@ -101,7 +101,7 @@ --> Error Loading Links Internal Storage Options + Dub + Sub + Delete File + Resume Download + Pause Download \ No newline at end of file