mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
subtitle download and other sub settings
This commit is contained in:
parent
440a9810be
commit
10f25eaefe
11 changed files with 547 additions and 229 deletions
|
@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.R
|
|||
import com.lagradost.cloudstream3.ui.player.PlayerFragment
|
||||
import com.lagradost.cloudstream3.ui.player.UriData
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
|
@ -69,7 +70,10 @@ object DownloadButtonSetup {
|
|||
val info =
|
||||
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(act, click.data.id)
|
||||
?: return
|
||||
|
||||
val keyInfo = act.getKey<VideoDownloadManager.DownloadedFileInfo>(
|
||||
VideoDownloadManager.KEY_DOWNLOAD_INFO,
|
||||
click.data.id.toString()
|
||||
) ?: return
|
||||
(act as FragmentActivity).supportFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(
|
||||
R.anim.enter_anim,
|
||||
|
@ -82,6 +86,8 @@ object DownloadButtonSetup {
|
|||
PlayerFragment.newInstance(
|
||||
UriData(
|
||||
info.path.toString(),
|
||||
keyInfo.relativePath,
|
||||
keyInfo.displayName,
|
||||
click.data.id,
|
||||
headerName ?: "null",
|
||||
if (click.data.episode <= 0) null else click.data.episode,
|
||||
|
|
|
@ -66,6 +66,7 @@ import com.lagradost.cloudstream3.ui.result.ResultViewModel
|
|||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.getFocusRequest
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.getVideoContentUri
|
||||
|
@ -78,6 +79,7 @@ import com.lagradost.cloudstream3.utils.DataStore.setKey
|
|||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getNavigationBarHeight
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
|
@ -86,7 +88,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import com.lagradost.cloudstream3.utils.VIDEO_PLAYER_BRIGHTNESS
|
||||
import com.lagradost.cloudstream3.utils.getId
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getId
|
||||
import kotlinx.android.synthetic.main.fragment_player.*
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.*
|
||||
import kotlinx.coroutines.*
|
||||
|
@ -140,6 +143,8 @@ data class PlayerData(
|
|||
|
||||
data class UriData(
|
||||
val uri: String,
|
||||
val relativePath: String,
|
||||
val displayName: String,
|
||||
val id: Int?,
|
||||
val name: String,
|
||||
val episode: Int?,
|
||||
|
@ -280,7 +285,8 @@ class PlayerFragment : Fragment() {
|
|||
fadeAnimation.fillAfter = true
|
||||
|
||||
subView?.let { sView ->
|
||||
val move = if (isShowing) -((bottom_player_bar?.height?.toFloat() ?: 0f) + 10.toPx) else -subStyle.elevation.toPx.toFloat()
|
||||
val move = if (isShowing) -((bottom_player_bar?.height?.toFloat()
|
||||
?: 0f) + 10.toPx) else -subStyle.elevation.toPx.toFloat()
|
||||
ObjectAnimator.ofFloat(sView, "translationY", move).apply {
|
||||
duration = 200
|
||||
start()
|
||||
|
@ -810,13 +816,40 @@ class PlayerFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setPreferredSubLanguage(lang: String?) {
|
||||
//val textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT) ?: return@setOnClickListener
|
||||
val realLang = if (lang.isNullOrBlank()) "" else lang
|
||||
preferredSubtitles =
|
||||
if (realLang.length == 2) SubtitleHelper.fromTwoLettersToLanguage(realLang) ?: realLang else realLang
|
||||
|
||||
if (!this::exoPlayer.isInitialized) return
|
||||
(exoPlayer?.trackSelector as DefaultTrackSelector?)?.let { trackSelector ->
|
||||
if (lang.isNullOrBlank()) {
|
||||
trackSelector.setParameters(
|
||||
trackSelector.buildUponParameters()
|
||||
.setPreferredTextLanguage(realLang)
|
||||
//.setRendererDisabled(textRendererIndex, true)
|
||||
)
|
||||
} else {
|
||||
trackSelector.setParameters(
|
||||
trackSelector.buildUponParameters()
|
||||
.setPreferredTextLanguage(realLang)
|
||||
//.setRendererDisabled(textRendererIndex, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.let { ctx ->
|
||||
setPreferredSubLanguage(ctx.getAutoSelectLanguageISO639_1())
|
||||
}
|
||||
|
||||
subView = player_view.findViewById(R.id.exo_subtitles)
|
||||
subView?.let { sView ->
|
||||
(sView.parent as ViewGroup?) ?.removeView(sView)
|
||||
(sView.parent as ViewGroup?)?.removeView(sView)
|
||||
subtitle_holder.addView(sView)
|
||||
}
|
||||
|
||||
|
@ -860,7 +893,7 @@ class PlayerFragment : Fragment() {
|
|||
epData.index,
|
||||
episodes,
|
||||
links,
|
||||
getSubs() ?: ArrayList(),
|
||||
context?.getSubs(supportsDownloadedFiles = false) ?: emptyList(),
|
||||
index,
|
||||
exoPlayer.currentPosition
|
||||
)
|
||||
|
@ -929,7 +962,12 @@ class PlayerFragment : Fragment() {
|
|||
}
|
||||
|
||||
sources_btt.visibility =
|
||||
if (isDownloadedFile) GONE else VISIBLE
|
||||
if (isDownloadedFile)
|
||||
if (context?.getSubs()?.isNullOrEmpty() != false)
|
||||
GONE else VISIBLE
|
||||
else VISIBLE
|
||||
|
||||
|
||||
player_media_route_button.visibility =
|
||||
if (isDownloadedFile) GONE else VISIBLE
|
||||
if (savedInstanceState != null) {
|
||||
|
@ -1154,136 +1192,97 @@ class PlayerFragment : Fragment() {
|
|||
}
|
||||
|
||||
sources_btt.setOnClickListener {
|
||||
lateinit var dialog: AlertDialog
|
||||
getUrls()?.let { it1 ->
|
||||
sortUrls(it1).let { sources ->
|
||||
val isPlaying = exoPlayer.isPlaying
|
||||
exoPlayer.pause()
|
||||
val currentSubtitles = activeSubtitles
|
||||
val isPlaying = exoPlayer.isPlaying
|
||||
exoPlayer.pause()
|
||||
val currentSubtitles = activeSubtitles
|
||||
|
||||
val sourceBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack)
|
||||
.setView(R.layout.player_select_source_and_subs)
|
||||
val sourceBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack)
|
||||
.setView(R.layout.player_select_source_and_subs)
|
||||
|
||||
val sourceDialog = sourceBuilder.create()
|
||||
sourceDialog.show()
|
||||
// bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet)
|
||||
val providerList = sourceDialog.findViewById<ListView>(R.id.sort_providers)!!
|
||||
val subtitleList = sourceDialog.findViewById<ListView>(R.id.sort_subtitles)!!
|
||||
val applyButton = sourceDialog.findViewById<MaterialButton>(R.id.apply_btt)!!
|
||||
val cancelButton = sourceDialog.findViewById<MaterialButton>(R.id.cancel_btt)!!
|
||||
val subsSettings = sourceDialog.findViewById<View>(R.id.subs_settings)!!
|
||||
val sourceDialog = sourceBuilder.create()
|
||||
sourceDialog.show()
|
||||
// bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet)
|
||||
val providerList = sourceDialog.findViewById<ListView>(R.id.sort_providers)!!
|
||||
val subtitleList = sourceDialog.findViewById<ListView>(R.id.sort_subtitles)!!
|
||||
val applyButton = sourceDialog.findViewById<MaterialButton>(R.id.apply_btt)!!
|
||||
val cancelButton = sourceDialog.findViewById<MaterialButton>(R.id.cancel_btt)!!
|
||||
val subsSettings = sourceDialog.findViewById<View>(R.id.subs_settings)!!
|
||||
|
||||
subsSettings.setOnClickListener {
|
||||
SubtitlesFragment.push(activity)
|
||||
sourceDialog.dismiss()
|
||||
}
|
||||
subsSettings.setOnClickListener {
|
||||
SubtitlesFragment.push(activity)
|
||||
sourceDialog.dismiss()
|
||||
}
|
||||
var sourceIndex = 0
|
||||
var startSource = 0
|
||||
var sources: List<ExtractorLink> = emptyList()
|
||||
|
||||
val startSource = sources.indexOf(getCurrentUrl())
|
||||
var sourceIndex = startSource
|
||||
val startSubtitle = currentSubtitles.indexOf(preferredSubtitles) + 1
|
||||
var subtitleIndex = startSubtitle
|
||||
val nonSortedUrls = getUrls()
|
||||
if (nonSortedUrls.isNullOrEmpty()) {
|
||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.visibility = GONE
|
||||
} else {
|
||||
sources = sortUrls(nonSortedUrls)
|
||||
startSource = sources.indexOf(getCurrentUrl())
|
||||
sourceIndex = startSource
|
||||
|
||||
if (currentSubtitles.isEmpty()) {
|
||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE
|
||||
} else {
|
||||
val subsArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
|
||||
subsArrayAdapter.add("No Subtitles")
|
||||
subsArrayAdapter.addAll(currentSubtitles)
|
||||
val sourcesArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
|
||||
sourcesArrayAdapter.addAll(sources.map { it.name })
|
||||
|
||||
subtitleList.adapter = subsArrayAdapter
|
||||
subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
providerList.adapter = sourcesArrayAdapter
|
||||
providerList.setSelection(sourceIndex)
|
||||
providerList.setItemChecked(sourceIndex, true)
|
||||
|
||||
subtitleList.setSelection(subtitleIndex)
|
||||
subtitleList.setItemChecked(subtitleIndex, true)
|
||||
|
||||
subtitleList.setOnItemClickListener { _, _, which, _ ->
|
||||
subtitleIndex = which
|
||||
subtitleList.setItemChecked(which, true)
|
||||
}
|
||||
}
|
||||
|
||||
val sourcesArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
|
||||
sourcesArrayAdapter.addAll(sources.map { it.name })
|
||||
|
||||
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
providerList.adapter = sourcesArrayAdapter
|
||||
providerList.setSelection(sourceIndex)
|
||||
providerList.setItemChecked(sourceIndex, true)
|
||||
|
||||
providerList.setOnItemClickListener { _, _, which, _ ->
|
||||
sourceIndex = which
|
||||
providerList.setItemChecked(which, true)
|
||||
}
|
||||
|
||||
sourceDialog.setOnDismissListener {
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
|
||||
cancelButton.setOnClickListener {
|
||||
sourceDialog.dismiss()
|
||||
}
|
||||
|
||||
applyButton.setOnClickListener {
|
||||
if (sourceIndex != startSource) {
|
||||
playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0
|
||||
setMirrorId(sources[sourceIndex].getId())
|
||||
initPlayer(getCurrentUrl())
|
||||
} else {
|
||||
if (isPlaying) {
|
||||
// exoPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
if (subtitleIndex != startSubtitle) {
|
||||
val textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT) ?: return@setOnClickListener
|
||||
(exoPlayer.trackSelector as DefaultTrackSelector?)?.let { trackSelector ->
|
||||
if (subtitleIndex <= 0) {
|
||||
preferredSubtitles = ""
|
||||
trackSelector.setParameters(
|
||||
trackSelector.buildUponParameters()
|
||||
.setPreferredTextLanguage("")
|
||||
.setRendererDisabled(textRendererIndex, true)
|
||||
)
|
||||
} else {
|
||||
val currentPreferredSub = currentSubtitles[subtitleIndex - 1]
|
||||
preferredSubtitles = currentPreferredSub
|
||||
trackSelector.setParameters(
|
||||
trackSelector.buildUponParameters()
|
||||
.setPreferredTextLanguage(currentPreferredSub)
|
||||
.setRendererDisabled(textRendererIndex, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceDialog.dismiss()
|
||||
}
|
||||
/*
|
||||
|
||||
*/
|
||||
/*
|
||||
|
||||
val sourcesText = sources.map { it.name }
|
||||
val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
|
||||
builder.setTitle("Pick source")
|
||||
builder.setOnDismissListener {
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
builder.setSingleChoiceItems(
|
||||
sourcesText.toTypedArray(),
|
||||
sources.indexOf(getCurrentUrl())
|
||||
) { _, which ->
|
||||
//val speed = speedsText[which]
|
||||
//Toast.makeText(requireContext(), "$speed selected.", Toast.LENGTH_SHORT).show()
|
||||
playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0
|
||||
setMirrorId(sources[which].getId())
|
||||
initPlayer(getCurrentUrl())
|
||||
|
||||
dialog.dismiss()
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
dialog = builder.create()
|
||||
dialog.show()*/
|
||||
providerList.setOnItemClickListener { _, _, which, _ ->
|
||||
sourceIndex = which
|
||||
providerList.setItemChecked(which, true)
|
||||
}
|
||||
|
||||
sourceDialog.setOnDismissListener {
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
}
|
||||
|
||||
val startIndexFromMap = currentSubtitles.map { it.removeSuffix(" ") }.indexOf(preferredSubtitles.removeSuffix(" ")) + 1
|
||||
var subtitleIndex = startIndexFromMap
|
||||
|
||||
if (currentSubtitles.isEmpty()) {
|
||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE
|
||||
} else {
|
||||
val subsArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
|
||||
subsArrayAdapter.add("No Subtitles")
|
||||
subsArrayAdapter.addAll(currentSubtitles)
|
||||
|
||||
subtitleList.adapter = subsArrayAdapter
|
||||
subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
|
||||
subtitleList.setSelection(subtitleIndex)
|
||||
subtitleList.setItemChecked(subtitleIndex, true)
|
||||
|
||||
subtitleList.setOnItemClickListener { _, _, which, _ ->
|
||||
subtitleIndex = which
|
||||
subtitleList.setItemChecked(which, true)
|
||||
}
|
||||
}
|
||||
|
||||
cancelButton.setOnClickListener {
|
||||
sourceDialog.dismiss()
|
||||
}
|
||||
|
||||
applyButton.setOnClickListener {
|
||||
if (sourceIndex != startSource) {
|
||||
playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0
|
||||
setMirrorId(sources[sourceIndex].getId())
|
||||
initPlayer(getCurrentUrl())
|
||||
} else {
|
||||
if (isPlaying) {
|
||||
// exoPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
if (subtitleIndex != startIndexFromMap) {
|
||||
setPreferredSubLanguage(if (subtitleIndex <= 0) null else currentSubtitles[subtitleIndex - 1])
|
||||
}
|
||||
sourceDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1347,9 +1346,25 @@ class PlayerFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getSubs(): List<SubtitleFile>? {
|
||||
private fun Context.getSubs(supportsDownloadedFiles: Boolean = true): List<SubtitleFile>? {
|
||||
return try {
|
||||
allEpisodesSubs[getEpisode()?.id]
|
||||
if (isDownloadedFile) {
|
||||
if (!supportsDownloadedFiles) return null
|
||||
val list = ArrayList<SubtitleFile>()
|
||||
VideoDownloadManager.getFolder(this, uriData.relativePath)?.forEach { file ->
|
||||
val name = uriData.displayName.removeSuffix(".mp4")
|
||||
if (file.first != uriData.displayName && file.first.startsWith(name)) {
|
||||
val realName = file.first.removePrefix(name)
|
||||
.removeSuffix(".vtt")
|
||||
.removeSuffix(".srt")
|
||||
.removeSuffix(".txt")
|
||||
list.add(SubtitleFile(realName.ifBlank { "Default" }, file.second.toString()))
|
||||
}
|
||||
}
|
||||
return list
|
||||
} else {
|
||||
allEpisodesSubs[getEpisode()?.id]
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
@ -1599,28 +1614,26 @@ class PlayerFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
val subs = getSubs()
|
||||
if (subs != null) {
|
||||
val subItems = ArrayList<MediaItem.Subtitle>()
|
||||
val subItemsId = ArrayList<String>()
|
||||
val subs = context?.getSubs() ?: emptyList()
|
||||
val subItems = ArrayList<MediaItem.Subtitle>()
|
||||
val subItemsId = ArrayList<String>()
|
||||
|
||||
for (sub in sortSubs(subs)) {
|
||||
val langId = sub.lang //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang
|
||||
subItemsId.add(langId)
|
||||
subItems.add(
|
||||
MediaItem.Subtitle(
|
||||
Uri.parse(sub.url),
|
||||
sub.url.toSubtitleMimeType(),
|
||||
langId,
|
||||
C.SELECTION_FLAG_DEFAULT
|
||||
)
|
||||
for (sub in sortSubs(subs)) {
|
||||
val langId = sub.lang //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang
|
||||
subItemsId.add(langId)
|
||||
subItems.add(
|
||||
MediaItem.Subtitle(
|
||||
Uri.parse(sub.url),
|
||||
sub.url.toSubtitleMimeType(),
|
||||
langId,
|
||||
C.SELECTION_FLAG_DEFAULT
|
||||
)
|
||||
}
|
||||
|
||||
activeSubtitles = subItemsId
|
||||
mediaItemBuilder.setSubtitles(subItems)
|
||||
)
|
||||
}
|
||||
|
||||
activeSubtitles = subItemsId
|
||||
mediaItemBuilder.setSubtitles(subItems)
|
||||
|
||||
//might add https://github.com/ed828a/Aihua/blob/1896f46888b5a954b367e83f40b845ce174a2328/app/src/main/java/com/dew/aihua/player/playerUI/VideoPlayer.kt#L287 toggle caps
|
||||
|
||||
val mediaItem = mediaItemBuilder.build()
|
||||
|
@ -1825,6 +1838,16 @@ class PlayerFragment : Fragment() {
|
|||
})
|
||||
} catch (e: java.lang.IllegalStateException) {
|
||||
println("Warning: Illegal state exception in PlayerFragment")
|
||||
} finally {
|
||||
setPreferredSubLanguage(
|
||||
if(isDownloadedFile) {
|
||||
if(activeSubtitles.isNotEmpty()) {
|
||||
activeSubtitles.first()
|
||||
} else null
|
||||
} else {
|
||||
preferredSubtitles
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,9 +24,6 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.request.RequestOptions.bitmapTransform
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.cast.framework.CastState
|
||||
|
@ -52,6 +49,8 @@ import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
|
|||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerData
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerFragment
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
||||
|
@ -64,13 +63,11 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import kotlinx.android.synthetic.main.fragment_result.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
|
@ -239,7 +236,7 @@ class ResultFragment : Fragment() {
|
|||
var startAction: Int? = null
|
||||
|
||||
private fun lateFixDownloadButton(show: Boolean) {
|
||||
if(!show || currentType?.isMovieType() == false) {
|
||||
if (!show || currentType?.isMovieType() == false) {
|
||||
result_movie_parent.visibility = GONE
|
||||
result_episodes_text.visibility = VISIBLE
|
||||
result_episodes.visibility = VISIBLE
|
||||
|
@ -381,7 +378,11 @@ class ResultFragment : Fragment() {
|
|||
return false
|
||||
}
|
||||
|
||||
fun aquireSingeExtractorLink(links: List<ExtractorLink>, title: String, callback: (ExtractorLink) -> Unit) {
|
||||
fun acquireSingeExtractorLink(
|
||||
links: List<ExtractorLink>,
|
||||
title: String,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
|
||||
|
||||
builder.setTitle(title)
|
||||
|
@ -392,8 +393,8 @@ class ResultFragment : Fragment() {
|
|||
builder.create().show()
|
||||
}
|
||||
|
||||
fun aquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) {
|
||||
aquireSingeExtractorLink(currentLinks ?: return, title, callback)
|
||||
fun acquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) {
|
||||
acquireSingeExtractorLink(currentLinks ?: return, title, callback)
|
||||
}
|
||||
|
||||
fun startChromecast(startIndex: Int) {
|
||||
|
@ -412,7 +413,7 @@ class ResultFragment : Fragment() {
|
|||
)
|
||||
}
|
||||
|
||||
fun startDownload(links: List<ExtractorLink>) {
|
||||
fun startDownload(links: List<ExtractorLink>, subs: List<SubtitleFile>?) {
|
||||
val isMovie = currentIsMovie ?: return
|
||||
val titleName = sanitizeFilename(currentHeaderName ?: return)
|
||||
|
||||
|
@ -481,6 +482,36 @@ class ResultFragment : Fragment() {
|
|||
meta,
|
||||
links
|
||||
)
|
||||
// 1. Checks if the lang should be downloaded
|
||||
// 2. Makes it into the download format
|
||||
// 3. Downloads it as a .vtt file
|
||||
val downloadList = ctx.getDownloadSubsLanguageISO639_1()
|
||||
main {
|
||||
subs?.let { subsList ->
|
||||
subsList.filter { downloadList.contains(SubtitleHelper.fromLanguageToTwoLetters(it.lang)) }
|
||||
.map { ExtractorSubtitleLink(it.lang, it.url, "") }
|
||||
.forEach { link ->
|
||||
val epName = meta.name ?: "Episode ${meta.episode}"
|
||||
val fileName =
|
||||
sanitizeFilename(epName + if (downloadList.size > 1) " ${link.name}" else "")
|
||||
val topFolder = "$folder"
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
VideoDownloadManager.downloadThing(
|
||||
ctx,
|
||||
link,
|
||||
fileName,
|
||||
topFolder,
|
||||
"vtt",
|
||||
false,
|
||||
null
|
||||
) {
|
||||
// no notification
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -547,10 +578,10 @@ class ResultFragment : Fragment() {
|
|||
dialog.show()
|
||||
}
|
||||
ACTION_COPY_LINK -> {
|
||||
aquireSingeExtractorLink("Copy Link") { link ->
|
||||
acquireSingeExtractorLink("Copy Link") { link ->
|
||||
val serviceClipboard =
|
||||
(requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?)
|
||||
?: return@aquireSingeExtractorLink
|
||||
?: return@acquireSingeExtractorLink
|
||||
val clip = ClipData.newPlainText(link.name, link.url)
|
||||
serviceClipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(requireContext(), "Text Copied", Toast.LENGTH_SHORT).show()
|
||||
|
@ -558,7 +589,7 @@ class ResultFragment : Fragment() {
|
|||
}
|
||||
|
||||
ACTION_PLAY_EPISODE_IN_BROWSER -> {
|
||||
aquireSingeExtractorLink("Play in Browser") { link ->
|
||||
acquireSingeExtractorLink("Play in Browser") { link ->
|
||||
val i = Intent(ACTION_VIEW)
|
||||
i.data = Uri.parse(link.url)
|
||||
startActivity(i)
|
||||
|
@ -566,7 +597,7 @@ class ResultFragment : Fragment() {
|
|||
}
|
||||
|
||||
ACTION_CHROME_CAST_MIRROR -> {
|
||||
aquireSingeExtractorLink("Cast Mirror") { link ->
|
||||
acquireSingeExtractorLink("Cast Mirror") { link ->
|
||||
val mirrorIndex = currentLinks?.indexOf(link) ?: -1
|
||||
startChromecast(if (mirrorIndex == -1) 0 else mirrorIndex)
|
||||
}
|
||||
|
@ -657,15 +688,15 @@ class ResultFragment : Fragment() {
|
|||
}
|
||||
|
||||
ACTION_DOWNLOAD_EPISODE -> {
|
||||
startDownload(currentLinks ?: return@main)
|
||||
startDownload(currentLinks ?: return@main, currentSubs)
|
||||
}
|
||||
|
||||
ACTION_DOWNLOAD_MIRROR -> {
|
||||
aquireSingeExtractorLink(
|
||||
acquireSingeExtractorLink(
|
||||
(currentLinks ?: return@main).filter { !it.isM3u8 },
|
||||
"Download Mirror"
|
||||
) { link ->
|
||||
startDownload(listOf(link))
|
||||
startDownload(listOf(link), currentSubs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -704,7 +735,7 @@ class ResultFragment : Fragment() {
|
|||
}
|
||||
|
||||
observe(viewModel.episodes) { episodeList ->
|
||||
lateFixDownloadButton( episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this
|
||||
lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this
|
||||
|
||||
when (startAction) {
|
||||
START_ACTION_RESUME_LATEST -> {
|
||||
|
|
|
@ -26,12 +26,17 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey
|
|||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
import kotlinx.android.synthetic.main.subtitle_settings.*
|
||||
|
||||
const val SUBTITLE_KEY = "subtitle_settings"
|
||||
const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select"
|
||||
const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download"
|
||||
|
||||
data class SaveCaptionStyle(
|
||||
var foregroundColor: Int,
|
||||
|
@ -111,6 +116,14 @@ class SubtitlesFragment : Fragment() {
|
|||
val metrics: DisplayMetrics = Resources.getSystem().displayMetrics
|
||||
return TypedValue.applyDimension(unit, size, metrics).toInt()
|
||||
}
|
||||
|
||||
fun Context.getDownloadSubsLanguageISO639_1(): List<String> {
|
||||
return getKey(SUBTITLE_DOWNLOAD_KEY) ?: listOf("en")
|
||||
}
|
||||
|
||||
fun Context.getAutoSelectLanguageISO639_1(): String {
|
||||
return getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en"
|
||||
}
|
||||
}
|
||||
|
||||
private fun onColorSelected(stuff: Pair<Int, Int>) {
|
||||
|
@ -296,10 +309,57 @@ class SubtitlesFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
subs_font.setOnLongClickListener {
|
||||
subs_font.setOnLongClickListener { textView ->
|
||||
state.typeface = null
|
||||
it.context.updateState()
|
||||
Toast.makeText(it.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show()
|
||||
textView.context.updateState()
|
||||
Toast.makeText(textView.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show()
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
subs_auto_select_language.setOnClickListener { textView ->
|
||||
val langMap = arrayListOf(
|
||||
SubtitleHelper.Language639("None", "None", "", "", "", "", ""),
|
||||
)
|
||||
langMap.addAll(SubtitleHelper.languages)
|
||||
|
||||
val lang639_1 = langMap.map { it.ISO_639_1 }
|
||||
textView.context.showDialog(
|
||||
langMap.map { it.languageName },
|
||||
lang639_1.indexOf(textView.context.getAutoSelectLanguageISO639_1()),
|
||||
(textView as TextView).text.toString(),
|
||||
true,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
textView.context.setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index])
|
||||
}
|
||||
}
|
||||
|
||||
subs_auto_select_language.setOnLongClickListener { textView ->
|
||||
textView.context.setKey(SUBTITLE_AUTO_SELECT_KEY, "en")
|
||||
Toast.makeText(textView.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show()
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
subs_download_languages.setOnClickListener { textView ->
|
||||
val langMap = SubtitleHelper.languages
|
||||
val lang639_1 = langMap.map { it.ISO_639_1 }
|
||||
val keys = textView.context.getDownloadSubsLanguageISO639_1()
|
||||
val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 }
|
||||
|
||||
textView.context.showMultiDialog(
|
||||
langMap.map { it.languageName },
|
||||
keyMap,
|
||||
(textView as TextView).text.toString(),
|
||||
dismissCallback
|
||||
) { indexList ->
|
||||
textView.context.setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList())
|
||||
}
|
||||
}
|
||||
|
||||
subs_download_languages.setOnLongClickListener { textView ->
|
||||
textView.context.setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en"))
|
||||
|
||||
Toast.makeText(textView.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show()
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
|
|
|
@ -6,15 +6,17 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
|||
data class ExtractorLink(
|
||||
val source: String,
|
||||
val name: String,
|
||||
val url: String,
|
||||
val referer: String,
|
||||
override val url: String,
|
||||
override val referer: String,
|
||||
val quality: Int,
|
||||
val isM3u8: Boolean = false,
|
||||
)
|
||||
) : VideoDownloadManager.IDownloadableMinimum
|
||||
|
||||
fun ExtractorLink.getId(): Int {
|
||||
return url.hashCode()
|
||||
}
|
||||
data class ExtractorSubtitleLink(
|
||||
val name: String,
|
||||
override val url: String,
|
||||
override val referer: String,
|
||||
) : VideoDownloadManager.IDownloadableMinimum
|
||||
|
||||
enum class Qualities(var value: Int) {
|
||||
Unknown(0),
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
|||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.util.forEach
|
||||
import androidx.core.view.marginLeft
|
||||
import androidx.core.view.marginRight
|
||||
import androidx.core.view.marginTop
|
||||
|
@ -15,20 +16,22 @@ object SingleSelectionHelper {
|
|||
fun Context.showDialog(
|
||||
dialog: Dialog,
|
||||
items: List<String>,
|
||||
selectedIndex: Int,
|
||||
selectedIndex: List<Int>,
|
||||
name: String,
|
||||
showApply: Boolean,
|
||||
callback: (Int) -> Unit,
|
||||
isMultiSelect: Boolean,
|
||||
callback: (List<Int>) -> Unit,
|
||||
dismissCallback: () -> Unit
|
||||
) {
|
||||
val realShowApply = showApply || isMultiSelect
|
||||
val listView = dialog.findViewById<ListView>(R.id.listview1)!!
|
||||
val textView = dialog.findViewById<TextView>(R.id.text1)!!
|
||||
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
|
||||
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
|
||||
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
|
||||
|
||||
applyHolder.visibility = if (showApply) View.VISIBLE else View.GONE
|
||||
if (!showApply) {
|
||||
applyHolder.visibility = if (realShowApply) View.VISIBLE else View.GONE
|
||||
if (!realShowApply) {
|
||||
val params = listView.layoutParams as LinearLayout.LayoutParams
|
||||
params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0)
|
||||
listView.layoutParams = params
|
||||
|
@ -40,29 +43,45 @@ object SingleSelectionHelper {
|
|||
arrayAdapter.addAll(items)
|
||||
|
||||
listView.adapter = arrayAdapter
|
||||
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
if (isMultiSelect) {
|
||||
listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
||||
} else {
|
||||
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
}
|
||||
|
||||
listView.setSelection(selectedIndex)
|
||||
listView.setItemChecked(selectedIndex, true)
|
||||
for (select in selectedIndex) {
|
||||
listView.setItemChecked(select, true)
|
||||
}
|
||||
|
||||
var currentIndex = selectedIndex
|
||||
selectedIndex.minOrNull()?.let {
|
||||
listView.setSelection(it)
|
||||
}
|
||||
|
||||
// var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
dismissCallback.invoke()
|
||||
}
|
||||
|
||||
listView.setOnItemClickListener { _, _, which, _ ->
|
||||
if (showApply) {
|
||||
currentIndex = which
|
||||
listView.setItemChecked(which, true)
|
||||
// lastSelectedIndex = which
|
||||
if (realShowApply) {
|
||||
if (!isMultiSelect) {
|
||||
listView.setItemChecked(which, true)
|
||||
}
|
||||
} else {
|
||||
callback.invoke(which)
|
||||
callback.invoke(listOf(which))
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
if (showApply) {
|
||||
if (realShowApply) {
|
||||
applyButton.setOnClickListener {
|
||||
callback.invoke(currentIndex)
|
||||
val list = ArrayList<Int>()
|
||||
for (index in 0 until listView.count) {
|
||||
if (listView.checkedItemPositions[index])
|
||||
list.add(index)
|
||||
}
|
||||
callback.invoke(list)
|
||||
dialog.dismiss()
|
||||
}
|
||||
cancelButton.setOnClickListener {
|
||||
|
@ -71,6 +90,21 @@ object SingleSelectionHelper {
|
|||
}
|
||||
}
|
||||
|
||||
fun Context.showMultiDialog(
|
||||
items: List<String>,
|
||||
selectedIndex: List<Int>,
|
||||
name: String,
|
||||
dismissCallback: () -> Unit,
|
||||
callback: (List<Int>) -> Unit,
|
||||
) {
|
||||
val builder =
|
||||
AlertDialog.Builder(this, R.style.AlertDialogCustom).setView(R.layout.bottom_selection_dialog)
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
showDialog(dialog, items, selectedIndex, name, true, true, callback, dismissCallback)
|
||||
}
|
||||
|
||||
fun Context.showDialog(
|
||||
items: List<String>,
|
||||
selectedIndex: Int,
|
||||
|
@ -84,7 +118,16 @@ object SingleSelectionHelper {
|
|||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
showDialog(dialog, items, selectedIndex, name, showApply, callback, dismissCallback)
|
||||
showDialog(
|
||||
dialog,
|
||||
items,
|
||||
listOf(selectedIndex),
|
||||
name,
|
||||
showApply,
|
||||
false,
|
||||
{ if (it.isNotEmpty()) callback.invoke(it.first()) },
|
||||
dismissCallback
|
||||
)
|
||||
}
|
||||
|
||||
fun Context.showBottomDialog(
|
||||
|
@ -100,6 +143,15 @@ object SingleSelectionHelper {
|
|||
builder.setContentView(R.layout.bottom_selection_dialog)
|
||||
|
||||
builder.show()
|
||||
showDialog(builder, items, selectedIndex, name, showApply, callback, dismissCallback)
|
||||
showDialog(
|
||||
builder,
|
||||
items,
|
||||
listOf(selectedIndex),
|
||||
name,
|
||||
showApply,
|
||||
false,
|
||||
{ callback.invoke(it.first()) },
|
||||
dismissCallback
|
||||
)
|
||||
}
|
||||
}
|
|
@ -207,7 +207,7 @@ object SubtitleHelper {
|
|||
Language639("Malayalam", "മലയാളം", "ml", "mal", "mal", "mal", ""),
|
||||
Language639("Maltese", "Malti", "mt", "mlt", "mlt", "mlt", ""),
|
||||
Language639("Māori", "te reo Māori", "mi", "mri", "", "mri", ""),
|
||||
Language639("Marathi (Marāṭhī)", "मराठी", "mr", "mar", "mar", "mar", ""),
|
||||
Language639("Marathi", "मराठी", "mr", "mar", "mar", "mar", ""),
|
||||
Language639("Marshallese", "Kajin M̧ajeļ", "mh", "mah", "mah", "mah", ""),
|
||||
Language639("Mongolian", "Монгол хэл", "mn", "mon", "mon", "mon", ""),
|
||||
Language639("Nauruan", "Dorerin Naoero", "na", "nau", "nau", "nau", ""),
|
||||
|
@ -238,7 +238,7 @@ object SubtitleHelper {
|
|||
Language639("Reunion Creole", "Kréol Rénioné", "rc", "rcf", "rcf", "rcf", ""),
|
||||
Language639("Romanian", "limba română", "ro", "ron", "", "ron", ""),
|
||||
Language639("Russian", "Русский", "ru", "rus", "rus", "rus", ""),
|
||||
Language639("Sanskrit (Saṁskṛta)", "संस्कृतम्", "sa", "san", "san", "san", ""),
|
||||
Language639("Sanskrit", "संस्कृतम्", "sa", "san", "san", "san", ""),
|
||||
Language639("Sardinian", "sardu", "sc", "srd", "srd", "srd", ""),
|
||||
Language639("Sindhi", "सिन्धी, سنڌي، سندھی", "sd", "snd", "snd", "snd", ""),
|
||||
Language639("Northern Sami", "Davvisámegiella", "se", "sme", "sme", "sme", ""),
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.RequiresApi
|
||||
|
@ -27,6 +28,7 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey
|
|||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getExistingDownloadUriOrNullQ
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -35,6 +37,7 @@ import java.lang.Thread.sleep
|
|||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
|
||||
const val DOWNLOAD_CHANNEL_NAME = "Downloads"
|
||||
|
@ -85,6 +88,15 @@ object VideoDownloadManager {
|
|||
Stop,
|
||||
}
|
||||
|
||||
interface IDownloadableMinimum {
|
||||
val url: String
|
||||
val referer: String
|
||||
}
|
||||
|
||||
fun VideoDownloadManager.IDownloadableMinimum.getId(): Int {
|
||||
return url.hashCode()
|
||||
}
|
||||
|
||||
data class DownloadEpisodeMetadata(
|
||||
val id: Int,
|
||||
val mainName: String,
|
||||
|
@ -126,7 +138,7 @@ object VideoDownloadManager {
|
|||
|
||||
private const val SUCCESS_DOWNLOAD_DONE = 1
|
||||
private const val SUCCESS_STOPPED = 2
|
||||
private const val ERROR_DELETING_FILE = -1
|
||||
private const val ERROR_DELETING_FILE = 3 // will not download the next one, but is still classified as an error
|
||||
private const val ERROR_CREATE_FILE = -2
|
||||
private const val ERROR_OPEN_FILE = -3
|
||||
private const val ERROR_TOO_SMALL_CONNECTION = -4
|
||||
|
@ -191,7 +203,7 @@ object VideoDownloadManager {
|
|||
cachedBitmaps[url] = bitmap
|
||||
}
|
||||
return null
|
||||
} catch (e : Exception) {
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -361,6 +373,69 @@ object VideoDownloadManager {
|
|||
return tempName.replace(" ", " ").trim(' ')
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun ContentResolver.getExistingFolderStartName(relativePath: String): List<Pair<String, Uri>>? {
|
||||
try {
|
||||
val projection = arrayOf(
|
||||
MediaStore.MediaColumns._ID,
|
||||
MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only)
|
||||
//MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only)
|
||||
)
|
||||
|
||||
val selection =
|
||||
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath'"
|
||||
|
||||
val result = this.query(
|
||||
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
|
||||
projection, selection, null, null
|
||||
)
|
||||
val list = ArrayList<Pair<String, Uri>>()
|
||||
|
||||
result.use { c ->
|
||||
if (c != null && c.count >= 1) {
|
||||
c.moveToFirst()
|
||||
while (true) {
|
||||
val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
||||
val name = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
|
||||
val uri = ContentUris.withAppendedId(
|
||||
MediaStore.Downloads.EXTERNAL_CONTENT_URI, id
|
||||
)
|
||||
list.add(Pair(name, uri))
|
||||
if (c.isLast) {
|
||||
break
|
||||
}
|
||||
c.moveToNext()
|
||||
}
|
||||
|
||||
/*
|
||||
val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
|
||||
val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/
|
||||
|
||||
}
|
||||
}
|
||||
return list
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getFolder(context: Context, relativePath: String): List<Pair<String, Uri>>? {
|
||||
if (isScopedStorage()) {
|
||||
return context.contentResolver?.getExistingFolderStartName(relativePath)
|
||||
} else {
|
||||
val normalPath =
|
||||
"${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace(
|
||||
'/',
|
||||
File.separatorChar
|
||||
)
|
||||
val folder = File(normalPath)
|
||||
if (folder.isDirectory) {
|
||||
return folder.listFiles().map { Pair(it.name, it.toUri()) }
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? {
|
||||
try {
|
||||
|
@ -412,18 +487,24 @@ object VideoDownloadManager {
|
|||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
}
|
||||
|
||||
private fun downloadSingleEpisode(
|
||||
context: Context,
|
||||
source: String?,
|
||||
folder: String?,
|
||||
ep: DownloadEpisodeMetadata,
|
||||
link: ExtractorLink,
|
||||
tryResume: Boolean = false,
|
||||
): Int {
|
||||
val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}")
|
||||
data class CreateNotificationMetadata(
|
||||
val type: DownloadType,
|
||||
val bytesDownloaded: Long,
|
||||
val bytesTotal: Long,
|
||||
)
|
||||
|
||||
fun downloadThing(
|
||||
context: Context,
|
||||
link: IDownloadableMinimum,
|
||||
name: String,
|
||||
folder: String?,
|
||||
extension: String,
|
||||
tryResume: Boolean,
|
||||
parentId: Int?,
|
||||
createNotificationCallback: (CreateNotificationMetadata) -> Unit
|
||||
): Int {
|
||||
val relativePath = (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
|
||||
val displayName = "$name.mp4"
|
||||
val displayName = "$name.$extension"
|
||||
|
||||
val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
|
||||
var resume = tryResume
|
||||
|
@ -440,7 +521,9 @@ object VideoDownloadManager {
|
|||
} else {
|
||||
if (!File(normalPath).delete()) return ERROR_DELETING_FILE
|
||||
}
|
||||
downloadDeleteEvent.invoke(ep.id)
|
||||
parentId?.let {
|
||||
downloadDeleteEvent.invoke(parentId)
|
||||
}
|
||||
return SUCCESS_STOPPED
|
||||
}
|
||||
|
||||
|
@ -468,11 +551,18 @@ object VideoDownloadManager {
|
|||
} else {
|
||||
val contentUri =
|
||||
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
|
||||
//val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
val currentMimeType = when (extension) {
|
||||
"vtt" -> "text/vtt"
|
||||
"mp4" -> "video/mp4"
|
||||
"srt" -> "text/plain"
|
||||
else -> null
|
||||
}
|
||||
val newFile = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||
put(MediaStore.MediaColumns.TITLE, name)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
|
||||
if (currentMimeType != null)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||
}
|
||||
|
||||
|
@ -539,9 +629,11 @@ object VideoDownloadManager {
|
|||
}
|
||||
val bytesTotal = contentLength + resumeLength
|
||||
|
||||
if (bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
|
||||
if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
|
||||
|
||||
context.setKey(KEY_DOWNLOAD_INFO, ep.id.toString(), DownloadedFileInfo(bytesTotal, relativePath, displayName))
|
||||
parentId?.let {
|
||||
context.setKey(KEY_DOWNLOAD_INFO, it.toString(), DownloadedFileInfo(bytesTotal, relativePath, displayName))
|
||||
}
|
||||
|
||||
// Could use connection.contentType for mime types when creating the file,
|
||||
// however file is already created and players don't go of file type
|
||||
|
@ -573,15 +665,18 @@ object VideoDownloadManager {
|
|||
else -> DownloadType.IsDownloading
|
||||
}
|
||||
|
||||
try {
|
||||
downloadStatus[ep.id] = type
|
||||
downloadStatusEvent.invoke(Pair(ep.id, type))
|
||||
downloadProgressEvent.invoke(Triple(ep.id, bytesDownloaded, bytesTotal))
|
||||
} catch (e: Exception) {
|
||||
// IDK MIGHT ERROR
|
||||
parentId?.let { id ->
|
||||
try {
|
||||
downloadStatus[id] = type
|
||||
downloadStatusEvent.invoke(Pair(id, type))
|
||||
downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal))
|
||||
} catch (e: Exception) {
|
||||
// IDK MIGHT ERROR
|
||||
}
|
||||
}
|
||||
|
||||
createNotification(
|
||||
createNotificationCallback.invoke(CreateNotificationMetadata(type, bytesDownloaded, bytesTotal))
|
||||
/*createNotification(
|
||||
context,
|
||||
source,
|
||||
link.name,
|
||||
|
@ -589,12 +684,11 @@ object VideoDownloadManager {
|
|||
type,
|
||||
bytesDownloaded,
|
||||
bytesTotal
|
||||
)
|
||||
)*/
|
||||
}
|
||||
|
||||
|
||||
val downloadEventListener = { event: Pair<Int, DownloadActionType> ->
|
||||
if (event.first == ep.id) {
|
||||
if (event.first == parentId) {
|
||||
when (event.second) {
|
||||
DownloadActionType.Pause -> {
|
||||
isPaused = true; updateNotification()
|
||||
|
@ -611,7 +705,8 @@ object VideoDownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
downloadEvent += downloadEventListener
|
||||
if (parentId != null)
|
||||
downloadEvent += downloadEventListener
|
||||
|
||||
// UPDATE DOWNLOAD NOTIFICATION
|
||||
val notificationCoroutine = main {
|
||||
|
@ -625,7 +720,6 @@ object VideoDownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
val id = ep.id
|
||||
// THE REAL READ
|
||||
try {
|
||||
while (true) {
|
||||
|
@ -655,13 +749,16 @@ object VideoDownloadManager {
|
|||
notificationCoroutine.cancel()
|
||||
|
||||
try {
|
||||
downloadEvent -= downloadEventListener
|
||||
if (parentId != null)
|
||||
downloadEvent -= downloadEventListener
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
try {
|
||||
downloadStatus.remove(ep.id)
|
||||
parentId?.let {
|
||||
downloadStatus.remove(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// IDK MIGHT ERROR
|
||||
}
|
||||
|
@ -669,15 +766,15 @@ object VideoDownloadManager {
|
|||
// RETURN MESSAGE
|
||||
return when {
|
||||
isFailed -> {
|
||||
downloadProgressEvent.invoke(Triple(id, 0, 0))
|
||||
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
|
||||
ERROR_CONNECTION_ERROR
|
||||
}
|
||||
isStopped -> {
|
||||
downloadProgressEvent.invoke(Triple(id, 0, 0))
|
||||
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
|
||||
deleteFile()
|
||||
}
|
||||
else -> {
|
||||
downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal))
|
||||
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) }
|
||||
isDone = true
|
||||
updateNotification()
|
||||
SUCCESS_DOWNLOAD_DONE
|
||||
|
@ -685,6 +782,29 @@ object VideoDownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
private fun downloadSingleEpisode(
|
||||
context: Context,
|
||||
source: String?,
|
||||
folder: String?,
|
||||
ep: DownloadEpisodeMetadata,
|
||||
link: ExtractorLink,
|
||||
tryResume: Boolean = false,
|
||||
): Int {
|
||||
val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}")
|
||||
|
||||
return downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta ->
|
||||
createNotification(
|
||||
context,
|
||||
source,
|
||||
link.name,
|
||||
ep,
|
||||
meta.type,
|
||||
meta.bytesDownloaded,
|
||||
meta.bytesTotal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadCheck(context: Context) {
|
||||
if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) {
|
||||
val pkg = downloadQueue.removeFirst()
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sort_sources_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textStyle="bold"
|
||||
|
@ -78,6 +78,26 @@
|
|||
android:text="@string/subs_subtitle_elevation"
|
||||
style="@style/SettingsItem"
|
||||
/>
|
||||
<TextView
|
||||
android:id="@+id/subs_auto_select_language"
|
||||
android:text="@string/subs_auto_select_language"
|
||||
style="@style/SettingsItem"
|
||||
/>
|
||||
<TextView
|
||||
android:id="@+id/subs_download_languages"
|
||||
android:text="@string/subs_download_languages"
|
||||
style="@style/SettingsItem"
|
||||
/>
|
||||
<TextView
|
||||
android:gravity="center"
|
||||
android:text="@string/subs_hold_to_reset_to_default"
|
||||
android:textSize="14sp"
|
||||
|
||||
android:textColor="?attr/textColor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_height="wrap_content">
|
||||
</TextView>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
|
|
|
@ -88,4 +88,7 @@
|
|||
<string name="manual_check_update_key">manual_check_update</string>
|
||||
|
||||
<string name="prerelease_commit_hash">unknown_prerelease</string>
|
||||
<string name="subs_auto_select_language">Auto Select Language</string>
|
||||
<string name="subs_download_languages">Download Languages</string>
|
||||
<string name="subs_hold_to_reset_to_default">Hold to reset to default</string>
|
||||
</resources>
|
Loading…
Reference in a new issue