3
3
Fork 1
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:
CranberrySoup 2024-07-04 17:45:40 +00:00 committed by GitHub
commit c16ba812e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 369 additions and 235 deletions

View file

@ -80,13 +80,13 @@ body:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
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.
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
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.
required: true

View file

@ -2,7 +2,7 @@ blank_issues_enabled: false
contact_links:
- name: Request a new provider or report bug with an existing provider
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
url: https://discord.gg/5Hus6fM
about: Join our discord for faster support on smaller issues.

View file

@ -27,9 +27,7 @@ body:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
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.
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
required: true

View file

@ -14,7 +14,6 @@ plugins {
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
var isLibraryDebug = false
fun String.execute() = ByteArrayOutputStream().use { baot ->
if (project.exec {
@ -105,7 +104,6 @@ android {
)
}
debug {
isLibraryDebug = true
isDebuggable = true
applicationIdSuffix = ".debug"
proguardFiles(
@ -236,7 +234,14 @@ dependencies {
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
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)
})
}

View file

@ -2,6 +2,7 @@ package com.lagradost.cloudstream3
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.extractor.downloader.Request
import org.schabi.newpipe.extractor.downloader.Response
@ -18,7 +19,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
val dataToSend: ByteArray? = request.dataToSend()
var requestBody: RequestBody? = null
if (dataToSend != null) {
requestBody = RequestBody.create(null, dataToSend)
requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size)
}
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.method(httpMethod, requestBody).url(url)

View file

@ -15,7 +15,9 @@ import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
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.getViewPos
import com.lagradost.cloudstream3.utils.UIHelper.setImage
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_LONG_CLICK = 5
const val DOWNLOAD_ACTION_GO_TO_CHILD = 0
const val DOWNLOAD_ACTION_LOAD_RESULT = 1
abstract class VisualDownloadCached(
open val currentBytes: Long,
open val totalBytes: Long,
@ -93,110 +98,128 @@ class DownloadAdapter(
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
is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadHeaderCached)
is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadChildCached)
}
}
downloadHeaderPoster.apply {
setImage(d.poster)
setOnClickListener {
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
}
}
@SuppressLint("SetTextI18n")
private fun bindHeader(card: VisualDownloadHeaderCached?) {
if (binding !is DownloadHeaderEpisodeBinding) return
card ?: return
val d = card.data
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))
}
binding.apply {
downloadHeaderPoster.apply {
setImage(d.poster)
setOnClickListener {
clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_LOAD_RESULT, d))
}
}
is DownloadChildEpisodeBinding -> binding.apply {
if (card == null || card !is VisualDownloadChildCached) return@apply
val d = card.data
downloadHeaderTitle.text = d.name
val formattedSizeString = formatShortFileSize(itemView.context, card.totalBytes)
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
if (card.child != null) {
downloadHeaderGotoChild.isVisible = false
val status = downloadButton.getStatus(card.child.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(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)
downloadChildEpisodeText.apply {
text = context.getNameFull(d.name, d.episode, d.season)
isSelected = true // Needed for text repeating
episodeHolder.setOnClickListener {
clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_GO_TO_CHILD, d))
}
}
}
}
downloadChildEpisodeHolder.setOnClickListener {
mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d))
private fun bindChild(card: VisualDownloadChildCached?) {
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 {
val inflater = LayoutInflater.from(parent.context)
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
)
}
VIEW_TYPE_HEADER -> DownloadHeaderEpisodeBinding.inflate(inflater, parent, false)
VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false)
else -> throw IllegalArgumentException("Invalid view type")
}
return DownloadViewHolder(binding, clickCallback, mediaClickCallback)
@ -207,8 +230,11 @@ class DownloadAdapter(
}
override fun getItemViewType(position: Int): Int {
val card = getItem(position)
return if (card is VisualDownloadChildCached) VIEW_TYPE_CHILD else VIEW_TYPE_HEADER
return when (getItem(position)) {
is VisualDownloadChildCached -> VIEW_TYPE_CHILD
is VisualDownloadHeaderCached -> VIEW_TYPE_HEADER
else -> throw IllegalArgumentException("Invalid data type at position $position")
}
}
class DiffCallback : DiffUtil.ItemCallback<VisualDownloadCached>() {

View file

@ -35,6 +35,7 @@ class DownloadChildFragment : Fragment() {
override fun onDestroyView() {
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
downloadDeleteEventListener = null
binding = null
super.onDestroyView()
}

View file

@ -1,8 +1,11 @@
package com.lagradost.cloudstream3.ui.download
import android.app.Activity
import android.app.Dialog
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.os.Build
import android.os.Bundle
import android.text.format.Formatter.formatShortFileSize
@ -12,6 +15,7 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
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.GeneratorPlayer
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.setLinearListLayout
import com.lagradost.cloudstream3.ui.settings.Globals.TV
@ -60,16 +65,8 @@ class DownloadFragment : Fragment() {
this.layoutParams = param
}
private fun setList(list: List<VisualDownloadHeaderCached>) {
main {
(binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(list)
}
}
override fun onDestroyView() {
downloadDeleteEventListener?.let {
VideoDownloadManager.downloadDeleteEvent -= it
}
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
downloadDeleteEventListener = null
binding = null
super.onDestroyView()
@ -95,12 +92,10 @@ class DownloadFragment : Fragment() {
hideKeyboard()
binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV()
observe(downloadsViewModel.noDownloadsText) {
binding?.textNoDownloads?.text = it
}
observe(downloadsViewModel.headerCards) {
setList(it)
(binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(it)
binding?.downloadLoading?.isVisible = false
binding?.textNoDownloads?.isVisible = it.isEmpty()
}
observe(downloadsViewModel.availableBytes) {
updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree)
@ -137,9 +132,15 @@ class DownloadFragment : Fragment() {
)
}
binding?.downloadStreamButton?.apply {
isGone = isLayout(TV)
setOnClickListener { showStreamInputDialog(it.context) }
binding?.apply {
openLocalVideoButton.apply {
isGone = isLayout(TV)
setOnClickListener { openLocalVideo() }
}
downloadStreamButton.apply {
isGone = isLayout(TV)
setOnClickListener { showStreamInputDialog(it.context) }
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -153,7 +154,7 @@ class DownloadFragment : Fragment() {
private fun handleItemClick(click: DownloadHeaderClickEvent) {
when (click.action) {
0 -> {
DOWNLOAD_ACTION_GO_TO_CHILD -> {
if (!click.data.type.isMovieType()) {
val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString())
activity?.navigate(
@ -162,7 +163,7 @@ class DownloadFragment : Fragment() {
)
}
}
1 -> {
DOWNLOAD_ACTION_LOAD_RESULT -> {
(activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName)
}
}
@ -189,6 +190,22 @@ class DownloadFragment : Fragment() {
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) {
val dialog = Dialog(context, R.style.AlertDialogCustom)
val binding = StreamInputBinding.inflate(dialog.layoutInflater)
@ -247,4 +264,13 @@ class DownloadFragment : Fragment() {
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)
}
}

View file

@ -16,17 +16,11 @@ import com.lagradost.cloudstream3.utils.DataStore.getFolderName
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
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.launch
import kotlinx.coroutines.withContext
class DownloadViewModel : ViewModel() {
private val _noDownloadsText = MutableLiveData<String>().apply {
value = ""
}
val noDownloadsText: LiveData<String> = _noDownloadsText
private val _headerCards =
MutableLiveData<List<VisualDownloadHeaderCached>>().apply { listOf<VisualDownloadHeaderCached>() }
val headerCards: LiveData<List<VisualDownloadHeaderCached>> = _headerCards
@ -43,8 +37,8 @@ class DownloadViewModel : ViewModel() {
fun updateList(context: Context) = viewModelScope.launchSafe {
val children = withContext(Dispatchers.IO) {
val headers = context.getKeys(DOWNLOAD_EPISODE_CACHE)
headers.mapNotNull { context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(it) }
context.getKeys(DOWNLOAD_EPISODE_CACHE)
.mapNotNull { context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(it) }
.distinctBy { it.id } // Remove duplicates
}
@ -57,10 +51,10 @@ class DownloadViewModel : ViewModel() {
// Gets all children downloads
withContext(Dispatchers.IO) {
for (c in children) {
val childFile = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, c.id) ?: continue
children.forEach { c ->
val childFile = getDownloadFileInfoAndUpdateSettings(context, c.id) ?: return@forEach
if (childFile.fileLength <= 1) continue
if (childFile.fileLength <= 1) return@forEach
val len = childFile.totalBytes
val flen = childFile.fileLength

View file

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.ui.download.button
import android.content.Context
import android.text.format.Formatter
import android.text.format.Formatter.formatShortFileSize
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.TextView
@ -9,6 +9,8 @@ import androidx.annotation.LayoutRes
import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar
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
typealias DownloadStatusTell = VideoDownloadManager.DownloadType
@ -34,7 +36,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
lateinit var progressBar: ContentLoadingProgressBar
var progressText: TextView? = null
/*val gid: String? get() = sessionIdToGid[persistentId]
/* val gid: String? get() = sessionIdToGid[persistentId]
// used for resuming data
var _lastRequestOverride: UriRequest? = null
@ -44,7 +46,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
_lastRequestOverride = value
}
var files: List<AbstractClient.JsonFile> = emptyList()*/
var files: List<AbstractClient.JsonFile> = emptyList() */
protected var isZeroBytes: Boolean = true
fun inflate(@LayoutRes layout: Int) {
@ -55,9 +57,12 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
resetViewData()
}
var doSetProgress = true
open fun resetViewData() {
// lastRequest = null
isZeroBytes = true
doSetProgress = true
persistentId = null
}
@ -68,37 +73,45 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
persistentId = id
currentMetaData.id = id
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, id)?.let { savedData ->
val downloadedBytes = savedData.fileLength
val totalBytes = savedData.totalBytes
if (!doSetProgress) return
/*lastRequest = savedData.uriRequest
files = savedData.files
ioSafe {
val savedData = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, id)
var totalBytes: Long = 0
var downloadedBytes: Long = 0
for (file in savedData.files) {
downloadedBytes += file.completedLength
totalBytes += file.length
}*/
setProgress(downloadedBytes, totalBytes)
// some extra padding for just in case
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
mainWork {
if (savedData != null) {
val downloadedBytes = savedData.fileLength
val totalBytes = savedData.totalBytes
setProgress(downloadedBytes, totalBytes)
applyMetaData(id, downloadedBytes, totalBytes)
} else run { resetView() }
}
setStatus(status)
} ?: run {
resetView()
}
}
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) {
isZeroBytes = downloadedBytes == 0L
progressBar.post {
@ -124,13 +137,15 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
if (isZeroBytes) {
progressText?.isVisible = false
} else {
progressText?.apply {
val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes)
val totalMbString = Formatter.formatShortFileSize(context, totalBytes)
text =
//if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
context?.getString(R.string.download_size_format)
?.format(currentMbString, totalMbString)
if (doSetProgress) {
progressText?.apply {
val currentFormattedSizeString = formatShortFileSize(context, downloadedBytes)
val totalFormattedSizeString = formatShortFileSize(context, totalBytes)
text =
// if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
context?.getString(R.string.download_size_format)
?.format(currentFormattedSizeString, totalFormattedSizeString)
}
}
}
@ -167,8 +182,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
override fun onAttachedToWindow() {
VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent
//VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent
//VideoDownloadManager.downloadEvent += ::downloadEvent
// VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent
// VideoDownloadManager.downloadEvent += ::downloadEvent
VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent
val pid = persistentId
@ -182,8 +197,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
override fun onDetachedFromWindow() {
VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent
//VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent
//VideoDownloadManager.downloadEvent -= ::downloadEvent
// VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent
// VideoDownloadManager.downloadEvent -= ::downloadEvent
VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent
super.onDetachedFromWindow()
@ -198,5 +213,4 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
* Get a clean slate again, might be useful in recyclerview?
* */
abstract fun resetView()
}

View file

@ -13,7 +13,6 @@ import androidx.annotation.MainThread
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.R
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.KEY_RESUME_PACKAGES
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
BaseFetchButton(context, attributeSet) {
@ -303,6 +301,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
setStatus(null)
currentMetaData = DownloadMetadata(0, 0, 0, null)
isZeroBytes = true
doSetProgress = true
progressBar.progress = 0
}

View file

@ -1,8 +1,6 @@
package com.lagradost.cloudstream3.ui.player
import android.content.ContentUris
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
@ -12,10 +10,15 @@ import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.UIHelper.navigate
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"
class DownloadedPlayerActivity : AppCompatActivity() {
private val dTAG = "DownloadedPlayerAct"
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
CommonActivity.dispatchKeyEvent(this, event)?.let {
return it
@ -34,53 +37,18 @@ class DownloadedPlayerActivity : AppCompatActivity() {
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?) {
Log.i(DTAG, "onCreate")
CommonActivity.loadThemes(this)
super.onCreate(savedInstanceState)
CommonActivity.loadThemes(this)
CommonActivity.init(this)
setContentView(R.layout.empty_layout)
Log.i(dTAG, "onCreate")
val data = intent.data
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)
} catch (e: Exception) {
null
}
val cd = intent.clipData
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
if (item?.uri != null)
playUri(item.uri)
playUri(this, item.uri)
else if (url != null)
playLink(url)
playLink(this, url)
else if (data != null)
playUri(data)
playUri(this, data)
else if (extraText != null)
playLink(extraText)
playLink(this, extraText)
else {
finish()
return
}
} else if (data?.scheme == "content") {
playUri(data)
playUri(this, data)
} else {
finish()
return

View file

@ -3,9 +3,12 @@ package com.lagradost.cloudstream3.ui.player
import android.net.Uri
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.*
import java.net.URI
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorUri
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(
val uri: Uri,

View file

@ -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()
)
)
)
)
)
}
}

View file

@ -16,6 +16,8 @@ import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.TransactionTooLargeException
import android.util.Log
import android.view.*
@ -475,7 +477,23 @@ object UIHelper {
}
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 {

View file

@ -17,6 +17,7 @@ import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
@ -29,7 +30,6 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.services.VideoDownloadService
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
@ -225,10 +225,10 @@ object VideoDownloadManager {
return cachedBitmaps[url]
}
val bitmap = com.bumptech.glide.Glide.with(this)
val bitmap = Glide.with(this)
.asBitmap()
.load(GlideUrl(url) { headers ?: emptyMap() })
.into(720, 720)
.submit(720, 720)
.get()
if (bitmap != null) {
@ -1857,4 +1857,4 @@ object VideoDownloadManager {
@JsonProperty("ep") val ep: DownloadEpisodeMetadata,
@JsonProperty("links") val links: List<ExtractorLink>
)
}
}

View 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>

View file

@ -143,17 +143,14 @@
<TextView
android:id="@+id/text_no_downloads"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:layout_gravity="center"
android:layout_margin="30dp"
android:text="@string/downloads_empty"
android:gravity="center"
android:visibility="gone"
tools:visibility="visible" />
<!--
<ProgressBar
@ -198,11 +195,30 @@
</LinearLayout>
</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
android:id="@+id/download_stream_button"
style="@style/ExtendedFloatingActionButton"
android:text="@string/stream"
android:textColor="?attr/textColor"
app:icon="@drawable/netflix_play"
tools:ignore="ContentDescription" />
app:icon="@drawable/ic_network_stream"
android:contentDescription="@string/stream" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -149,8 +149,10 @@
<string name="download_canceled">Download Canceled</string>
<string name="download_done">Download Done</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="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="links_reloaded_toast">Links Reloaded</string>
<string name="download_storage_text">Internal Storage</string>
@ -339,6 +341,10 @@
<string name="livestreams">Livestreams</string>
<string name="nsfw">NSFW</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-->
<string name="movies_singular">Movie</string>
<string name="tv_series_singular">Series</string>

View file

@ -47,6 +47,11 @@ buildkonfig {
defaultConfigs {
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())
}
}
@ -74,4 +79,4 @@ publishing {
groupId = "com.lagradost.api"
}
}
}
}