mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge branch 'master' into lib
This commit is contained in:
commit
c16ba812e5
20 changed files with 369 additions and 235 deletions
4
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
4
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
|
@ -80,13 +80,13 @@ body:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you haven't done these steps.
|
||||||
options:
|
options:
|
||||||
|
- label: I am sure my issue is related to the app and **NOT some extension**.
|
||||||
|
required: true
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
|
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
|
||||||
required: true
|
required: true
|
||||||
- label: If related to a provider, I have checked the site and it works, but not the app.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -2,7 +2,7 @@ blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Request a new provider or report bug with an existing provider
|
- name: Request a new provider or report bug with an existing provider
|
||||||
url: https://github.com/recloudstream
|
url: https://github.com/recloudstream
|
||||||
about: Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
|
about: EXTREMELY IMPORTANT - Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
|
||||||
- name: Discord
|
- name: Discord
|
||||||
url: https://discord.gg/5Hus6fM
|
url: https://discord.gg/5Hus6fM
|
||||||
about: Join our discord for faster support on smaller issues.
|
about: Join our discord for faster support on smaller issues.
|
||||||
|
|
6
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
6
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
|
@ -27,9 +27,7 @@ body:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you haven't done these steps.
|
||||||
options:
|
options:
|
||||||
|
- label: My suggestion is **NOT** about adding a new provider
|
||||||
|
required: true
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ plugins {
|
||||||
|
|
||||||
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
||||||
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
|
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
|
||||||
var isLibraryDebug = false
|
|
||||||
|
|
||||||
fun String.execute() = ByteArrayOutputStream().use { baot ->
|
fun String.execute() = ByteArrayOutputStream().use { baot ->
|
||||||
if (project.exec {
|
if (project.exec {
|
||||||
|
@ -105,7 +104,6 @@ android {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
isLibraryDebug = true
|
|
||||||
isDebuggable = true
|
isDebuggable = true
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
|
@ -236,7 +234,14 @@ dependencies {
|
||||||
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
|
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
|
||||||
|
|
||||||
implementation(project(":library") {
|
implementation(project(":library") {
|
||||||
this.extra.set("isDebug", isLibraryDebug)
|
// There does not seem to be a good way of getting the android flavor.
|
||||||
|
val isDebug = gradle.startParameter.taskRequests.any { task ->
|
||||||
|
task.args.any { arg ->
|
||||||
|
arg.contains("debug", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.extra.set("isDebug", isDebug)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.lagradost.cloudstream3
|
||||||
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
import org.schabi.newpipe.extractor.downloader.Request
|
import org.schabi.newpipe.extractor.downloader.Request
|
||||||
import org.schabi.newpipe.extractor.downloader.Response
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
|
@ -18,7 +19,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
val dataToSend: ByteArray? = request.dataToSend()
|
val dataToSend: ByteArray? = request.dataToSend()
|
||||||
var requestBody: RequestBody? = null
|
var requestBody: RequestBody? = null
|
||||||
if (dataToSend != null) {
|
if (dataToSend != null) {
|
||||||
requestBody = RequestBody.create(null, dataToSend)
|
requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size)
|
||||||
}
|
}
|
||||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||||
.method(httpMethod, requestBody).url(url)
|
.method(httpMethod, requestBody).url(url)
|
||||||
|
|
|
@ -15,7 +15,9 @@ import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
|
import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
|
import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
|
|
||||||
|
@ -26,6 +28,9 @@ const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3
|
||||||
const val DOWNLOAD_ACTION_DOWNLOAD = 4
|
const val DOWNLOAD_ACTION_DOWNLOAD = 4
|
||||||
const val DOWNLOAD_ACTION_LONG_CLICK = 5
|
const val DOWNLOAD_ACTION_LONG_CLICK = 5
|
||||||
|
|
||||||
|
const val DOWNLOAD_ACTION_GO_TO_CHILD = 0
|
||||||
|
const val DOWNLOAD_ACTION_LOAD_RESULT = 1
|
||||||
|
|
||||||
abstract class VisualDownloadCached(
|
abstract class VisualDownloadCached(
|
||||||
open val currentBytes: Long,
|
open val currentBytes: Long,
|
||||||
open val totalBytes: Long,
|
open val totalBytes: Long,
|
||||||
|
@ -93,110 +98,128 @@ class DownloadAdapter(
|
||||||
private val mediaClickCallback: (DownloadClickEvent) -> Unit,
|
private val mediaClickCallback: (DownloadClickEvent) -> Unit,
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
fun bind(card: VisualDownloadCached?) {
|
fun bind(card: VisualDownloadCached?) {
|
||||||
when (binding) {
|
when (binding) {
|
||||||
is DownloadHeaderEpisodeBinding -> binding.apply {
|
is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadHeaderCached)
|
||||||
if (card == null || card !is VisualDownloadHeaderCached) return@apply
|
is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadChildCached)
|
||||||
val d = card.data
|
}
|
||||||
|
}
|
||||||
|
|
||||||
downloadHeaderPoster.apply {
|
@SuppressLint("SetTextI18n")
|
||||||
setImage(d.poster)
|
private fun bindHeader(card: VisualDownloadHeaderCached?) {
|
||||||
setOnClickListener {
|
if (binding !is DownloadHeaderEpisodeBinding) return
|
||||||
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
|
card ?: return
|
||||||
}
|
val d = card.data
|
||||||
}
|
|
||||||
|
|
||||||
downloadHeaderTitle.text = d.name
|
binding.apply {
|
||||||
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
|
downloadHeaderPoster.apply {
|
||||||
|
setImage(d.poster)
|
||||||
if (card.child != null) {
|
setOnClickListener {
|
||||||
downloadHeaderGotoChild.isVisible = false
|
clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_LOAD_RESULT, d))
|
||||||
|
|
||||||
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 {
|
downloadHeaderTitle.text = d.name
|
||||||
if (card == null || card !is VisualDownloadChildCached) return@apply
|
val formattedSizeString = formatShortFileSize(itemView.context, card.totalBytes)
|
||||||
val d = card.data
|
|
||||||
|
|
||||||
val posDur = DataStoreHelper.getViewPos(d.id)
|
if (card.child != null) {
|
||||||
downloadChildEpisodeProgress.apply {
|
downloadHeaderGotoChild.isVisible = false
|
||||||
if (posDur != null) {
|
|
||||||
val visualPos = posDur.fixVisual()
|
val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes)
|
||||||
max = (visualPos.duration / 1000).toInt()
|
if (status == DownloadStatusTell.IsDone) {
|
||||||
progress = (visualPos.position / 1000).toInt()
|
// We do this here instead if we are finished downloading
|
||||||
isVisible = true
|
// so that we can use the value from the view model
|
||||||
} else isVisible = false
|
// rather than extra unneeded disk operations and to prevent a
|
||||||
|
// delay in updating download icon state.
|
||||||
|
downloadButton.setProgress(card.currentBytes, card.totalBytes)
|
||||||
|
downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes)
|
||||||
|
// We will let the view model handle this
|
||||||
|
downloadButton.doSetProgress = false
|
||||||
|
downloadHeaderInfo.text = formattedSizeString
|
||||||
|
} else downloadButton.doSetProgress = true
|
||||||
|
|
||||||
|
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback)
|
||||||
|
downloadButton.isVisible = true
|
||||||
|
|
||||||
|
episodeHolder.setOnClickListener {
|
||||||
|
mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
downloadButton.isVisible = false
|
||||||
|
downloadHeaderGotoChild.isVisible = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
downloadHeaderInfo.text = downloadHeaderInfo.context.getString(R.string.extra_info_format)
|
||||||
|
.format(
|
||||||
|
card.totalDownloads,
|
||||||
|
downloadHeaderInfo.context.resources.getQuantityString(
|
||||||
|
R.plurals.episodes,
|
||||||
|
card.totalDownloads
|
||||||
|
),
|
||||||
|
formattedSizeString
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// You probably formatted incorrectly
|
||||||
|
downloadHeaderInfo.text = "Error"
|
||||||
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadButton.setDefaultClickListener(card.data, downloadChildEpisodeTextExtra, mediaClickCallback)
|
episodeHolder.setOnClickListener {
|
||||||
|
clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_GO_TO_CHILD, d))
|
||||||
downloadChildEpisodeText.apply {
|
|
||||||
text = context.getNameFull(d.name, d.episode, d.season)
|
|
||||||
isSelected = true // Needed for text repeating
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
downloadChildEpisodeHolder.setOnClickListener {
|
private fun bindChild(card: VisualDownloadChildCached?) {
|
||||||
mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d))
|
if (binding !is DownloadChildEpisodeBinding) return
|
||||||
|
card ?: return
|
||||||
|
val d = card.data
|
||||||
|
|
||||||
|
binding.apply {
|
||||||
|
val posDur = getViewPos(d.id)
|
||||||
|
downloadChildEpisodeProgress.apply {
|
||||||
|
isVisible = posDur != null
|
||||||
|
posDur?.let {
|
||||||
|
val visualPos = it.fixVisual()
|
||||||
|
max = (visualPos.duration / 1000).toInt()
|
||||||
|
progress = (visualPos.position / 1000).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val status = downloadButton.getStatus(d.id, card.currentBytes, card.totalBytes)
|
||||||
|
if (status == DownloadStatusTell.IsDone) {
|
||||||
|
// We do this here instead if we are finished downloading
|
||||||
|
// so that we can use the value from the view model
|
||||||
|
// rather than extra unneeded disk operations and to prevent a
|
||||||
|
// delay in updating download icon state.
|
||||||
|
downloadButton.setProgress(card.currentBytes, card.totalBytes)
|
||||||
|
downloadButton.applyMetaData(d.id, card.currentBytes, card.totalBytes)
|
||||||
|
// We will let the view model handle this
|
||||||
|
downloadButton.doSetProgress = false
|
||||||
|
downloadChildEpisodeTextExtra.text = formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes)
|
||||||
|
} else downloadButton.doSetProgress = true
|
||||||
|
|
||||||
|
downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback)
|
||||||
|
downloadButton.isVisible = true
|
||||||
|
|
||||||
|
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 {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
val binding = when (viewType) {
|
val binding = when (viewType) {
|
||||||
VIEW_TYPE_HEADER -> {
|
VIEW_TYPE_HEADER -> DownloadHeaderEpisodeBinding.inflate(inflater, parent, false)
|
||||||
DownloadHeaderEpisodeBinding.inflate(
|
VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false)
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
VIEW_TYPE_CHILD -> {
|
|
||||||
DownloadChildEpisodeBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> throw IllegalArgumentException("Invalid view type")
|
else -> throw IllegalArgumentException("Invalid view type")
|
||||||
}
|
}
|
||||||
return DownloadViewHolder(binding, clickCallback, mediaClickCallback)
|
return DownloadViewHolder(binding, clickCallback, mediaClickCallback)
|
||||||
|
@ -207,8 +230,11 @@ class DownloadAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
val card = getItem(position)
|
return when (getItem(position)) {
|
||||||
return if (card is VisualDownloadChildCached) VIEW_TYPE_CHILD else VIEW_TYPE_HEADER
|
is VisualDownloadChildCached -> VIEW_TYPE_CHILD
|
||||||
|
is VisualDownloadHeaderCached -> VIEW_TYPE_HEADER
|
||||||
|
else -> throw IllegalArgumentException("Invalid data type at position $position")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DiffCallback : DiffUtil.ItemCallback<VisualDownloadCached>() {
|
class DiffCallback : DiffUtil.ItemCallback<VisualDownloadCached>() {
|
||||||
|
|
|
@ -35,6 +35,7 @@ class DownloadChildFragment : Fragment() {
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
|
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
|
||||||
|
downloadDeleteEventListener = null
|
||||||
binding = null
|
binding = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package com.lagradost.cloudstream3.ui.download
|
package com.lagradost.cloudstream3.ui.download
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.format.Formatter.formatShortFileSize
|
import android.text.format.Formatter.formatShortFileSize
|
||||||
|
@ -12,6 +15,7 @@ import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
|
@ -30,6 +34,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownload
|
||||||
import com.lagradost.cloudstream3.ui.player.BasicLink
|
import com.lagradost.cloudstream3.ui.player.BasicLink
|
||||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||||
import com.lagradost.cloudstream3.ui.player.LinkGenerator
|
import com.lagradost.cloudstream3.ui.player.LinkGenerator
|
||||||
|
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri
|
||||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
@ -60,16 +65,8 @@ class DownloadFragment : Fragment() {
|
||||||
this.layoutParams = param
|
this.layoutParams = param
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setList(list: List<VisualDownloadHeaderCached>) {
|
|
||||||
main {
|
|
||||||
(binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
downloadDeleteEventListener?.let {
|
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
|
||||||
VideoDownloadManager.downloadDeleteEvent -= it
|
|
||||||
}
|
|
||||||
downloadDeleteEventListener = null
|
downloadDeleteEventListener = null
|
||||||
binding = null
|
binding = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
@ -95,12 +92,10 @@ class DownloadFragment : Fragment() {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV()
|
binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV()
|
||||||
|
|
||||||
observe(downloadsViewModel.noDownloadsText) {
|
|
||||||
binding?.textNoDownloads?.text = it
|
|
||||||
}
|
|
||||||
observe(downloadsViewModel.headerCards) {
|
observe(downloadsViewModel.headerCards) {
|
||||||
setList(it)
|
(binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(it)
|
||||||
binding?.downloadLoading?.isVisible = false
|
binding?.downloadLoading?.isVisible = false
|
||||||
|
binding?.textNoDownloads?.isVisible = it.isEmpty()
|
||||||
}
|
}
|
||||||
observe(downloadsViewModel.availableBytes) {
|
observe(downloadsViewModel.availableBytes) {
|
||||||
updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree)
|
updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree)
|
||||||
|
@ -137,9 +132,15 @@ class DownloadFragment : Fragment() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding?.downloadStreamButton?.apply {
|
binding?.apply {
|
||||||
isGone = isLayout(TV)
|
openLocalVideoButton.apply {
|
||||||
setOnClickListener { showStreamInputDialog(it.context) }
|
isGone = isLayout(TV)
|
||||||
|
setOnClickListener { openLocalVideo() }
|
||||||
|
}
|
||||||
|
downloadStreamButton.apply {
|
||||||
|
isGone = isLayout(TV)
|
||||||
|
setOnClickListener { showStreamInputDialog(it.context) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
@ -153,7 +154,7 @@ class DownloadFragment : Fragment() {
|
||||||
|
|
||||||
private fun handleItemClick(click: DownloadHeaderClickEvent) {
|
private fun handleItemClick(click: DownloadHeaderClickEvent) {
|
||||||
when (click.action) {
|
when (click.action) {
|
||||||
0 -> {
|
DOWNLOAD_ACTION_GO_TO_CHILD -> {
|
||||||
if (!click.data.type.isMovieType()) {
|
if (!click.data.type.isMovieType()) {
|
||||||
val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString())
|
val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString())
|
||||||
activity?.navigate(
|
activity?.navigate(
|
||||||
|
@ -162,7 +163,7 @@ class DownloadFragment : Fragment() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1 -> {
|
DOWNLOAD_ACTION_LOAD_RESULT -> {
|
||||||
(activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName)
|
(activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,6 +190,22 @@ class DownloadFragment : Fragment() {
|
||||||
view?.setLayoutWidth(bytes)
|
view?.setLayoutWidth(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openLocalVideo() {
|
||||||
|
val intent = Intent()
|
||||||
|
.setAction(Intent.ACTION_GET_CONTENT)
|
||||||
|
.setType("video/*")
|
||||||
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
.addFlags(FLAG_GRANT_READ_URI_PERMISSION) // Request temporary access
|
||||||
|
normalSafeApiCall {
|
||||||
|
videoResultLauncher.launch(
|
||||||
|
Intent.createChooser(
|
||||||
|
intent,
|
||||||
|
getString(R.string.open_local_video)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showStreamInputDialog(context: Context) {
|
private fun showStreamInputDialog(context: Context) {
|
||||||
val dialog = Dialog(context, R.style.AlertDialogCustom)
|
val dialog = Dialog(context, R.style.AlertDialogCustom)
|
||||||
val binding = StreamInputBinding.inflate(dialog.layoutInflater)
|
val binding = StreamInputBinding.inflate(dialog.layoutInflater)
|
||||||
|
@ -247,4 +264,13 @@ class DownloadFragment : Fragment() {
|
||||||
binding?.downloadStreamButton?.extend()
|
binding?.downloadStreamButton?.extend()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open local video from files using content provider x safeFile
|
||||||
|
private val videoResultLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||||
|
val selectedVideoUri = result?.data?.data ?: return@registerForActivityResult
|
||||||
|
playUri(activity ?: return@registerForActivityResult, selectedVideoUri)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -16,17 +16,11 @@ import com.lagradost.cloudstream3.utils.DataStore.getFolderName
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class DownloadViewModel : ViewModel() {
|
class DownloadViewModel : ViewModel() {
|
||||||
private val _noDownloadsText = MutableLiveData<String>().apply {
|
|
||||||
value = ""
|
|
||||||
}
|
|
||||||
val noDownloadsText: LiveData<String> = _noDownloadsText
|
|
||||||
|
|
||||||
private val _headerCards =
|
private val _headerCards =
|
||||||
MutableLiveData<List<VisualDownloadHeaderCached>>().apply { listOf<VisualDownloadHeaderCached>() }
|
MutableLiveData<List<VisualDownloadHeaderCached>>().apply { listOf<VisualDownloadHeaderCached>() }
|
||||||
val headerCards: LiveData<List<VisualDownloadHeaderCached>> = _headerCards
|
val headerCards: LiveData<List<VisualDownloadHeaderCached>> = _headerCards
|
||||||
|
@ -43,8 +37,8 @@ class DownloadViewModel : ViewModel() {
|
||||||
|
|
||||||
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)
|
context.getKeys(DOWNLOAD_EPISODE_CACHE)
|
||||||
headers.mapNotNull { context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(it) }
|
.mapNotNull { context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(it) }
|
||||||
.distinctBy { it.id } // Remove duplicates
|
.distinctBy { it.id } // Remove duplicates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,10 +51,10 @@ class DownloadViewModel : ViewModel() {
|
||||||
|
|
||||||
// Gets all children downloads
|
// Gets all children downloads
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
for (c in children) {
|
children.forEach { c ->
|
||||||
val childFile = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, c.id) ?: continue
|
val childFile = getDownloadFileInfoAndUpdateSettings(context, c.id) ?: return@forEach
|
||||||
|
|
||||||
if (childFile.fileLength <= 1) continue
|
if (childFile.fileLength <= 1) return@forEach
|
||||||
val len = childFile.totalBytes
|
val len = childFile.totalBytes
|
||||||
val flen = childFile.fileLength
|
val flen = childFile.fileLength
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.ui.download.button
|
package com.lagradost.cloudstream3.ui.download.button
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter.formatShortFileSize
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -9,6 +9,8 @@ import androidx.annotation.LayoutRes
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.ContentLoadingProgressBar
|
import androidx.core.widget.ContentLoadingProgressBar
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
|
|
||||||
typealias DownloadStatusTell = VideoDownloadManager.DownloadType
|
typealias DownloadStatusTell = VideoDownloadManager.DownloadType
|
||||||
|
@ -34,7 +36,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
lateinit var progressBar: ContentLoadingProgressBar
|
lateinit var progressBar: ContentLoadingProgressBar
|
||||||
var progressText: TextView? = null
|
var progressText: TextView? = null
|
||||||
|
|
||||||
/*val gid: String? get() = sessionIdToGid[persistentId]
|
/* val gid: String? get() = sessionIdToGid[persistentId]
|
||||||
|
|
||||||
// used for resuming data
|
// used for resuming data
|
||||||
var _lastRequestOverride: UriRequest? = null
|
var _lastRequestOverride: UriRequest? = null
|
||||||
|
@ -44,7 +46,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
_lastRequestOverride = value
|
_lastRequestOverride = value
|
||||||
}
|
}
|
||||||
|
|
||||||
var files: List<AbstractClient.JsonFile> = emptyList()*/
|
var files: List<AbstractClient.JsonFile> = emptyList() */
|
||||||
protected var isZeroBytes: Boolean = true
|
protected var isZeroBytes: Boolean = true
|
||||||
|
|
||||||
fun inflate(@LayoutRes layout: Int) {
|
fun inflate(@LayoutRes layout: Int) {
|
||||||
|
@ -55,9 +57,12 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
resetViewData()
|
resetViewData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var doSetProgress = true
|
||||||
|
|
||||||
open fun resetViewData() {
|
open fun resetViewData() {
|
||||||
// lastRequest = null
|
// lastRequest = null
|
||||||
isZeroBytes = true
|
isZeroBytes = true
|
||||||
|
doSetProgress = true
|
||||||
persistentId = null
|
persistentId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,37 +73,45 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
persistentId = id
|
persistentId = id
|
||||||
currentMetaData.id = id
|
currentMetaData.id = id
|
||||||
|
|
||||||
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, id)?.let { savedData ->
|
if (!doSetProgress) return
|
||||||
val downloadedBytes = savedData.fileLength
|
|
||||||
val totalBytes = savedData.totalBytes
|
|
||||||
|
|
||||||
/*lastRequest = savedData.uriRequest
|
ioSafe {
|
||||||
files = savedData.files
|
val savedData = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, id)
|
||||||
|
|
||||||
var totalBytes: Long = 0
|
mainWork {
|
||||||
var downloadedBytes: Long = 0
|
if (savedData != null) {
|
||||||
for (file in savedData.files) {
|
val downloadedBytes = savedData.fileLength
|
||||||
downloadedBytes += file.completedLength
|
val totalBytes = savedData.totalBytes
|
||||||
totalBytes += file.length
|
|
||||||
}*/
|
setProgress(downloadedBytes, totalBytes)
|
||||||
setProgress(downloadedBytes, totalBytes)
|
applyMetaData(id, downloadedBytes, totalBytes)
|
||||||
// some extra padding for just in case
|
} else run { resetView() }
|
||||||
val status = VideoDownloadManager.downloadStatus[id]
|
|
||||||
?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) DownloadStatusTell.IsDone else DownloadStatusTell.IsPaused
|
|
||||||
currentMetaData.apply {
|
|
||||||
this.id = id
|
|
||||||
this.downloadedLength = downloadedBytes
|
|
||||||
this.totalLength = totalBytes
|
|
||||||
this.status = status
|
|
||||||
}
|
}
|
||||||
setStatus(status)
|
|
||||||
} ?: run {
|
|
||||||
resetView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun setStatus(status: VideoDownloadManager.DownloadType?)
|
abstract fun setStatus(status: VideoDownloadManager.DownloadType?)
|
||||||
|
|
||||||
|
fun getStatus(id:Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell {
|
||||||
|
// some extra padding for just in case
|
||||||
|
return VideoDownloadManager.downloadStatus[id]
|
||||||
|
?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) {
|
||||||
|
DownloadStatusTell.IsDone
|
||||||
|
} else DownloadStatusTell.IsPaused
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyMetaData(id:Int, downloadedBytes: Long, totalBytes: Long) {
|
||||||
|
val status = getStatus(id, downloadedBytes, totalBytes)
|
||||||
|
|
||||||
|
currentMetaData.apply {
|
||||||
|
this.id = id
|
||||||
|
this.downloadedLength = downloadedBytes
|
||||||
|
this.totalLength = totalBytes
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
setStatus(status)
|
||||||
|
}
|
||||||
|
|
||||||
open fun setProgress(downloadedBytes: Long, totalBytes: Long) {
|
open fun setProgress(downloadedBytes: Long, totalBytes: Long) {
|
||||||
isZeroBytes = downloadedBytes == 0L
|
isZeroBytes = downloadedBytes == 0L
|
||||||
progressBar.post {
|
progressBar.post {
|
||||||
|
@ -124,13 +137,15 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
if (isZeroBytes) {
|
if (isZeroBytes) {
|
||||||
progressText?.isVisible = false
|
progressText?.isVisible = false
|
||||||
} else {
|
} else {
|
||||||
progressText?.apply {
|
if (doSetProgress) {
|
||||||
val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes)
|
progressText?.apply {
|
||||||
val totalMbString = Formatter.formatShortFileSize(context, totalBytes)
|
val currentFormattedSizeString = formatShortFileSize(context, downloadedBytes)
|
||||||
text =
|
val totalFormattedSizeString = formatShortFileSize(context, totalBytes)
|
||||||
//if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
|
text =
|
||||||
context?.getString(R.string.download_size_format)
|
// if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
|
||||||
?.format(currentMbString, totalMbString)
|
context?.getString(R.string.download_size_format)
|
||||||
|
?.format(currentFormattedSizeString, totalFormattedSizeString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,8 +182,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent
|
VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent
|
||||||
//VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent
|
// VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent
|
||||||
//VideoDownloadManager.downloadEvent += ::downloadEvent
|
// VideoDownloadManager.downloadEvent += ::downloadEvent
|
||||||
VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent
|
VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent
|
||||||
|
|
||||||
val pid = persistentId
|
val pid = persistentId
|
||||||
|
@ -182,8 +197,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent
|
VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent
|
||||||
//VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent
|
// VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent
|
||||||
//VideoDownloadManager.downloadEvent -= ::downloadEvent
|
// VideoDownloadManager.downloadEvent -= ::downloadEvent
|
||||||
VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent
|
VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent
|
||||||
|
|
||||||
super.onDetachedFromWindow()
|
super.onDetachedFromWindow()
|
||||||
|
@ -198,5 +213,4 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
* Get a clean slate again, might be useful in recyclerview?
|
* Get a clean slate again, might be useful in recyclerview?
|
||||||
* */
|
* */
|
||||||
abstract fun resetView()
|
abstract fun resetView()
|
||||||
|
|
||||||
}
|
}
|
|
@ -13,7 +13,6 @@ import androidx.annotation.MainThread
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
@ -29,7 +28,6 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES
|
||||||
|
|
||||||
|
|
||||||
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
BaseFetchButton(context, attributeSet) {
|
BaseFetchButton(context, attributeSet) {
|
||||||
|
|
||||||
|
@ -303,6 +301,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
setStatus(null)
|
setStatus(null)
|
||||||
currentMetaData = DownloadMetadata(0, 0, 0, null)
|
currentMetaData = DownloadMetadata(0, 0, 0, null)
|
||||||
isZeroBytes = true
|
isZeroBytes = true
|
||||||
|
doSetProgress = true
|
||||||
progressBar.progress = 0
|
progressBar.progress = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
@ -12,10 +10,15 @@ import com.lagradost.cloudstream3.CommonActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
import com.lagradost.safefile.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
|
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink
|
||||||
|
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri
|
||||||
|
|
||||||
const val DTAG = "PlayerActivity"
|
const val DTAG = "PlayerActivity"
|
||||||
|
|
||||||
class DownloadedPlayerActivity : AppCompatActivity() {
|
class DownloadedPlayerActivity : AppCompatActivity() {
|
||||||
|
private val dTAG = "DownloadedPlayerAct"
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
CommonActivity.dispatchKeyEvent(this, event)?.let {
|
CommonActivity.dispatchKeyEvent(this, event)?.let {
|
||||||
return it
|
return it
|
||||||
|
@ -34,53 +37,18 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
||||||
CommonActivity.onUserLeaveHint(this)
|
CommonActivity.onUserLeaveHint(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playLink(url: String) {
|
|
||||||
this.navigate(
|
|
||||||
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
|
||||||
LinkGenerator(
|
|
||||||
listOf(
|
|
||||||
BasicLink(url)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun playUri(uri: Uri) {
|
|
||||||
val name = SafeFile.fromUri(this, uri)?.name()
|
|
||||||
this.navigate(
|
|
||||||
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
|
||||||
DownloadFileGenerator(
|
|
||||||
listOf(
|
|
||||||
ExtractorUri(
|
|
||||||
uri = uri,
|
|
||||||
name = name ?: getString(R.string.downloaded_file),
|
|
||||||
// well not the same as a normal id, but we take it as users may want to
|
|
||||||
// play downloaded files and save the location
|
|
||||||
id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Log.i(DTAG, "onCreate")
|
|
||||||
|
|
||||||
CommonActivity.loadThemes(this)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
CommonActivity.loadThemes(this)
|
||||||
CommonActivity.init(this)
|
CommonActivity.init(this)
|
||||||
|
|
||||||
setContentView(R.layout.empty_layout)
|
setContentView(R.layout.empty_layout)
|
||||||
|
Log.i(dTAG, "onCreate")
|
||||||
|
|
||||||
val data = intent.data
|
val data = intent.data
|
||||||
|
|
||||||
if (intent?.action == Intent.ACTION_SEND) {
|
if (intent?.action == Intent.ACTION_SEND) {
|
||||||
val extraText = try { // I dont trust android
|
val extraText = normalSafeApiCall { // I dont trust android
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)
|
intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
val cd = intent.clipData
|
val cd = intent.clipData
|
||||||
val item = if (cd != null && cd.itemCount > 0) cd.getItemAt(0) else null
|
val item = if (cd != null && cd.itemCount > 0) cd.getItemAt(0) else null
|
||||||
|
@ -88,19 +56,19 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
// idk what I am doing, just hope any of these work
|
// idk what I am doing, just hope any of these work
|
||||||
if (item?.uri != null)
|
if (item?.uri != null)
|
||||||
playUri(item.uri)
|
playUri(this, item.uri)
|
||||||
else if (url != null)
|
else if (url != null)
|
||||||
playLink(url)
|
playLink(this, url)
|
||||||
else if (data != null)
|
else if (data != null)
|
||||||
playUri(data)
|
playUri(this, data)
|
||||||
else if (extraText != null)
|
else if (extraText != null)
|
||||||
playLink(extraText)
|
playLink(this, extraText)
|
||||||
else {
|
else {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (data?.scheme == "content") {
|
} else if (data?.scheme == "content") {
|
||||||
playUri(data)
|
playUri(this, data)
|
||||||
} else {
|
} else {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
|
|
|
@ -3,9 +3,12 @@ package com.lagradost.cloudstream3.ui.player
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.amap
|
import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||||
import java.net.URI
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import com.lagradost.cloudstream3.utils.unshortenLinkSafe
|
||||||
|
|
||||||
data class ExtractorUri(
|
data class ExtractorUri(
|
||||||
val uri: Uri,
|
val uri: Uri,
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.content.ContextCompat.getString
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
|
import com.lagradost.safefile.SafeFile
|
||||||
|
|
||||||
|
object OfflinePlaybackHelper {
|
||||||
|
fun playLink(activity: Activity, url: String) {
|
||||||
|
activity.navigate(
|
||||||
|
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
||||||
|
LinkGenerator(
|
||||||
|
listOf(
|
||||||
|
BasicLink(url)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playUri(activity: Activity, uri: Uri) {
|
||||||
|
val name = SafeFile.fromUri(activity, uri)?.name()
|
||||||
|
activity.navigate(
|
||||||
|
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
||||||
|
DownloadFileGenerator(
|
||||||
|
listOf(
|
||||||
|
ExtractorUri(
|
||||||
|
uri = uri,
|
||||||
|
name = name ?: getString(activity, R.string.downloaded_file),
|
||||||
|
// well not the same as a normal id, but we take it as users may want to
|
||||||
|
// play downloaded files and save the location
|
||||||
|
id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,8 @@ import android.graphics.Color
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.os.TransactionTooLargeException
|
import android.os.TransactionTooLargeException
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
@ -475,7 +477,23 @@ object UIHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun FragmentActivity.popCurrentPage() {
|
fun FragmentActivity.popCurrentPage() {
|
||||||
this.onBackPressedDispatcher.onBackPressed()
|
// Post the back press action to the main thread handler to ensure it executes
|
||||||
|
// after any currently pending UI updates or fragment transactions.
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
// Check if the FragmentManager state is saved. If it is, we cannot perform
|
||||||
|
// fragment transactions safely because the state may be inconsistent.
|
||||||
|
if (!supportFragmentManager.isStateSaved) {
|
||||||
|
// If the state is not saved, it's safe to perform the back press action.
|
||||||
|
this.onBackPressedDispatcher.onBackPressed()
|
||||||
|
} else {
|
||||||
|
// If the state is saved, retry the back press action after a slight delay.
|
||||||
|
// This gives the FragmentManager time to complete any ongoing state-saving
|
||||||
|
// operations or transactions, ensuring that we do not encounter an IllegalStateException.
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
this.onBackPressedDispatcher.onBackPressed()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.getStatusBarHeight(): Int {
|
fun Context.getStatusBarHeight(): Int {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import androidx.work.Data
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
|
@ -29,7 +30,6 @@ import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
|
||||||
import com.lagradost.cloudstream3.services.VideoDownloadService
|
import com.lagradost.cloudstream3.services.VideoDownloadService
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
|
@ -225,10 +225,10 @@ object VideoDownloadManager {
|
||||||
return cachedBitmaps[url]
|
return cachedBitmaps[url]
|
||||||
}
|
}
|
||||||
|
|
||||||
val bitmap = com.bumptech.glide.Glide.with(this)
|
val bitmap = Glide.with(this)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(GlideUrl(url) { headers ?: emptyMap() })
|
.load(GlideUrl(url) { headers ?: emptyMap() })
|
||||||
.into(720, 720)
|
.submit(720, 720)
|
||||||
.get()
|
.get()
|
||||||
|
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
|
|
11
app/src/main/res/drawable/ic_network_stream.xml
Normal file
11
app/src/main/res/drawable/ic_network_stream.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/white"
|
||||||
|
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z" />
|
||||||
|
|
||||||
|
</vector>
|
|
@ -143,17 +143,14 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_no_downloads"
|
android:id="@+id/text_no_downloads"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_gravity="center"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_margin="30dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:text="@string/downloads_empty"
|
||||||
android:textAlignment="center"
|
android:gravity="center"
|
||||||
android:textSize="20sp"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
tools:visibility="visible" />
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
@ -198,11 +195,30 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.facebook.shimmer.ShimmerFrameLayout>
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_gravity="bottom|end">
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/open_local_video_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="?attr/floatingActionButtonSmallStyle"
|
||||||
|
android:backgroundTint="?attr/primaryGrayBackground"
|
||||||
|
android:src="@drawable/netflix_play"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:tooltipText="@string/open_local_video"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:contentDescription="@string/open_local_video" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
android:id="@+id/download_stream_button"
|
android:id="@+id/download_stream_button"
|
||||||
style="@style/ExtendedFloatingActionButton"
|
style="@style/ExtendedFloatingActionButton"
|
||||||
android:text="@string/stream"
|
android:text="@string/stream"
|
||||||
android:textColor="?attr/textColor"
|
android:textColor="?attr/textColor"
|
||||||
app:icon="@drawable/netflix_play"
|
app:icon="@drawable/ic_network_stream"
|
||||||
tools:ignore="ContentDescription" />
|
android:contentDescription="@string/stream" />
|
||||||
|
</LinearLayout>
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -149,8 +149,10 @@
|
||||||
<string name="download_canceled">Download Canceled</string>
|
<string name="download_canceled">Download Canceled</string>
|
||||||
<string name="download_done">Download Done</string>
|
<string name="download_done">Download Done</string>
|
||||||
<string name="download_format" translatable="false">%s - %s</string>
|
<string name="download_format" translatable="false">%s - %s</string>
|
||||||
|
<string name="downloads_empty">There are currently no downloads.</string>
|
||||||
<string name="update_started">Update Started</string>
|
<string name="update_started">Update Started</string>
|
||||||
<string name="stream">Network stream</string>
|
<string name="stream">Network stream</string>
|
||||||
|
<string name="open_local_video">Open local video</string>
|
||||||
<string name="error_loading_links_toast">Error Loading Links</string>
|
<string name="error_loading_links_toast">Error Loading Links</string>
|
||||||
<string name="links_reloaded_toast">Links Reloaded</string>
|
<string name="links_reloaded_toast">Links Reloaded</string>
|
||||||
<string name="download_storage_text">Internal Storage</string>
|
<string name="download_storage_text">Internal Storage</string>
|
||||||
|
@ -339,6 +341,10 @@
|
||||||
<string name="livestreams">Livestreams</string>
|
<string name="livestreams">Livestreams</string>
|
||||||
<string name="nsfw">NSFW</string>
|
<string name="nsfw">NSFW</string>
|
||||||
<string name="others">Others</string>
|
<string name="others">Others</string>
|
||||||
|
<plurals name="episodes" translatable="false">
|
||||||
|
<item quantity="one">@string/episode</item>
|
||||||
|
<item quantity="other">@string/episodes</item>
|
||||||
|
</plurals>
|
||||||
<!--singular-->
|
<!--singular-->
|
||||||
<string name="movies_singular">Movie</string>
|
<string name="movies_singular">Movie</string>
|
||||||
<string name="tv_series_singular">Series</string>
|
<string name="tv_series_singular">Series</string>
|
||||||
|
|
|
@ -47,6 +47,11 @@ buildkonfig {
|
||||||
|
|
||||||
defaultConfigs {
|
defaultConfigs {
|
||||||
val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true
|
val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true
|
||||||
|
if (isDebug) {
|
||||||
|
logger.quiet("Compiling library with debug flag")
|
||||||
|
} else {
|
||||||
|
logger.quiet("Compiling library with release flag")
|
||||||
|
}
|
||||||
buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString())
|
buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue