forked from recloudstream/cloudstream
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.PlayerFragment
|
||||||
import com.lagradost.cloudstream3.ui.player.UriData
|
import com.lagradost.cloudstream3.ui.player.UriData
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
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.DataStoreHelper.getViewPos
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
|
@ -69,7 +70,10 @@ object DownloadButtonSetup {
|
||||||
val info =
|
val info =
|
||||||
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(act, click.data.id)
|
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(act, click.data.id)
|
||||||
?: return
|
?: return
|
||||||
|
val keyInfo = act.getKey<VideoDownloadManager.DownloadedFileInfo>(
|
||||||
|
VideoDownloadManager.KEY_DOWNLOAD_INFO,
|
||||||
|
click.data.id.toString()
|
||||||
|
) ?: return
|
||||||
(act as FragmentActivity).supportFragmentManager.beginTransaction()
|
(act as FragmentActivity).supportFragmentManager.beginTransaction()
|
||||||
.setCustomAnimations(
|
.setCustomAnimations(
|
||||||
R.anim.enter_anim,
|
R.anim.enter_anim,
|
||||||
|
@ -82,6 +86,8 @@ object DownloadButtonSetup {
|
||||||
PlayerFragment.newInstance(
|
PlayerFragment.newInstance(
|
||||||
UriData(
|
UriData(
|
||||||
info.path.toString(),
|
info.path.toString(),
|
||||||
|
keyInfo.relativePath,
|
||||||
|
keyInfo.displayName,
|
||||||
click.data.id,
|
click.data.id,
|
||||||
headerName ?: "null",
|
headerName ?: "null",
|
||||||
if (click.data.episode <= 0) null else click.data.episode,
|
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.SaveCaptionStyle
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle
|
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.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.getFocusRequest
|
import com.lagradost.cloudstream3.utils.AppUtils.getFocusRequest
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.getVideoContentUri
|
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.DataStoreHelper.setViewPos
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
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.getNavigationBarHeight
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
|
import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
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.showSystemUI
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import com.lagradost.cloudstream3.utils.VIDEO_PLAYER_BRIGHTNESS
|
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.fragment_player.*
|
||||||
import kotlinx.android.synthetic.main.player_custom_layout.*
|
import kotlinx.android.synthetic.main.player_custom_layout.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
@ -140,6 +143,8 @@ data class PlayerData(
|
||||||
|
|
||||||
data class UriData(
|
data class UriData(
|
||||||
val uri: String,
|
val uri: String,
|
||||||
|
val relativePath: String,
|
||||||
|
val displayName: String,
|
||||||
val id: Int?,
|
val id: Int?,
|
||||||
val name: String,
|
val name: String,
|
||||||
val episode: Int?,
|
val episode: Int?,
|
||||||
|
@ -280,7 +285,8 @@ class PlayerFragment : Fragment() {
|
||||||
fadeAnimation.fillAfter = true
|
fadeAnimation.fillAfter = true
|
||||||
|
|
||||||
subView?.let { sView ->
|
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 {
|
ObjectAnimator.ofFloat(sView, "translationY", move).apply {
|
||||||
duration = 200
|
duration = 200
|
||||||
start()
|
start()
|
||||||
|
@ -810,9 +816,36 @@ 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")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
context?.let { ctx ->
|
||||||
|
setPreferredSubLanguage(ctx.getAutoSelectLanguageISO639_1())
|
||||||
|
}
|
||||||
|
|
||||||
subView = player_view.findViewById(R.id.exo_subtitles)
|
subView = player_view.findViewById(R.id.exo_subtitles)
|
||||||
subView?.let { sView ->
|
subView?.let { sView ->
|
||||||
|
@ -860,7 +893,7 @@ class PlayerFragment : Fragment() {
|
||||||
epData.index,
|
epData.index,
|
||||||
episodes,
|
episodes,
|
||||||
links,
|
links,
|
||||||
getSubs() ?: ArrayList(),
|
context?.getSubs(supportsDownloadedFiles = false) ?: emptyList(),
|
||||||
index,
|
index,
|
||||||
exoPlayer.currentPosition
|
exoPlayer.currentPosition
|
||||||
)
|
)
|
||||||
|
@ -929,7 +962,12 @@ class PlayerFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
sources_btt.visibility =
|
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 =
|
player_media_route_button.visibility =
|
||||||
if (isDownloadedFile) GONE else VISIBLE
|
if (isDownloadedFile) GONE else VISIBLE
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
|
@ -1154,9 +1192,6 @@ class PlayerFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
sources_btt.setOnClickListener {
|
sources_btt.setOnClickListener {
|
||||||
lateinit var dialog: AlertDialog
|
|
||||||
getUrls()?.let { it1 ->
|
|
||||||
sortUrls(it1).let { sources ->
|
|
||||||
val isPlaying = exoPlayer.isPlaying
|
val isPlaying = exoPlayer.isPlaying
|
||||||
exoPlayer.pause()
|
exoPlayer.pause()
|
||||||
val currentSubtitles = activeSubtitles
|
val currentSubtitles = activeSubtitles
|
||||||
|
@ -1177,11 +1212,38 @@ class PlayerFragment : Fragment() {
|
||||||
SubtitlesFragment.push(activity)
|
SubtitlesFragment.push(activity)
|
||||||
sourceDialog.dismiss()
|
sourceDialog.dismiss()
|
||||||
}
|
}
|
||||||
|
var sourceIndex = 0
|
||||||
|
var startSource = 0
|
||||||
|
var sources: List<ExtractorLink> = emptyList()
|
||||||
|
|
||||||
val startSource = sources.indexOf(getCurrentUrl())
|
val nonSortedUrls = getUrls()
|
||||||
var sourceIndex = startSource
|
if (nonSortedUrls.isNullOrEmpty()) {
|
||||||
val startSubtitle = currentSubtitles.indexOf(preferredSubtitles) + 1
|
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.visibility = GONE
|
||||||
var subtitleIndex = startSubtitle
|
} else {
|
||||||
|
sources = sortUrls(nonSortedUrls)
|
||||||
|
startSource = sources.indexOf(getCurrentUrl())
|
||||||
|
sourceIndex = startSource
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val startIndexFromMap = currentSubtitles.map { it.removeSuffix(" ") }.indexOf(preferredSubtitles.removeSuffix(" ")) + 1
|
||||||
|
var subtitleIndex = startIndexFromMap
|
||||||
|
|
||||||
if (currentSubtitles.isEmpty()) {
|
if (currentSubtitles.isEmpty()) {
|
||||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE
|
sourceDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE
|
||||||
|
@ -1202,23 +1264,6 @@ class PlayerFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
cancelButton.setOnClickListener {
|
||||||
sourceDialog.dismiss()
|
sourceDialog.dismiss()
|
||||||
}
|
}
|
||||||
|
@ -1234,57 +1279,11 @@ class PlayerFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subtitleIndex != startSubtitle) {
|
if (subtitleIndex != startIndexFromMap) {
|
||||||
val textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT) ?: return@setOnClickListener
|
setPreferredSubLanguage(if (subtitleIndex <= 0) null else currentSubtitles[subtitleIndex - 1])
|
||||||
(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()
|
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()*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
player_view.resizeMode = resizeModes[resizeMode]
|
player_view.resizeMode = resizeModes[resizeMode]
|
||||||
|
@ -1347,9 +1346,25 @@ class PlayerFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSubs(): List<SubtitleFile>? {
|
private fun Context.getSubs(supportsDownloadedFiles: Boolean = true): List<SubtitleFile>? {
|
||||||
return try {
|
return try {
|
||||||
|
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]
|
allEpisodesSubs[getEpisode()?.id]
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -1599,8 +1614,7 @@ class PlayerFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val subs = getSubs()
|
val subs = context?.getSubs() ?: emptyList()
|
||||||
if (subs != null) {
|
|
||||||
val subItems = ArrayList<MediaItem.Subtitle>()
|
val subItems = ArrayList<MediaItem.Subtitle>()
|
||||||
val subItemsId = ArrayList<String>()
|
val subItemsId = ArrayList<String>()
|
||||||
|
|
||||||
|
@ -1619,7 +1633,6 @@ class PlayerFragment : Fragment() {
|
||||||
|
|
||||||
activeSubtitles = subItemsId
|
activeSubtitles = subItemsId
|
||||||
mediaItemBuilder.setSubtitles(subItems)
|
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
|
//might add https://github.com/ed828a/Aihua/blob/1896f46888b5a954b367e83f40b845ce174a2328/app/src/main/java/com/dew/aihua/player/playerUI/VideoPlayer.kt#L287 toggle caps
|
||||||
|
|
||||||
|
@ -1825,6 +1838,16 @@ class PlayerFragment : Fragment() {
|
||||||
})
|
})
|
||||||
} catch (e: java.lang.IllegalStateException) {
|
} catch (e: java.lang.IllegalStateException) {
|
||||||
println("Warning: Illegal state exception in PlayerFragment")
|
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.lifecycle.ViewModelProvider
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.CastButtonFactory
|
||||||
import com.google.android.gms.cast.framework.CastContext
|
import com.google.android.gms.cast.framework.CastContext
|
||||||
import com.google.android.gms.cast.framework.CastState
|
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.download.DownloadButtonSetup.handleDownloadClick
|
||||||
import com.lagradost.cloudstream3.ui.player.PlayerData
|
import com.lagradost.cloudstream3.ui.player.PlayerData
|
||||||
import com.lagradost.cloudstream3.ui.player.PlayerFragment
|
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.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
|
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
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.UIHelper.setImage
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
||||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
|
||||||
import kotlinx.android.synthetic.main.fragment_result.*
|
import kotlinx.android.synthetic.main.fragment_result.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
|
@ -381,7 +378,11 @@ class ResultFragment : Fragment() {
|
||||||
return false
|
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)
|
val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
|
||||||
|
|
||||||
builder.setTitle(title)
|
builder.setTitle(title)
|
||||||
|
@ -392,8 +393,8 @@ class ResultFragment : Fragment() {
|
||||||
builder.create().show()
|
builder.create().show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) {
|
fun acquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) {
|
||||||
aquireSingeExtractorLink(currentLinks ?: return, title, callback)
|
acquireSingeExtractorLink(currentLinks ?: return, title, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startChromecast(startIndex: Int) {
|
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 isMovie = currentIsMovie ?: return
|
||||||
val titleName = sanitizeFilename(currentHeaderName ?: return)
|
val titleName = sanitizeFilename(currentHeaderName ?: return)
|
||||||
|
|
||||||
|
@ -481,6 +482,36 @@ class ResultFragment : Fragment() {
|
||||||
meta,
|
meta,
|
||||||
links
|
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()
|
dialog.show()
|
||||||
}
|
}
|
||||||
ACTION_COPY_LINK -> {
|
ACTION_COPY_LINK -> {
|
||||||
aquireSingeExtractorLink("Copy Link") { link ->
|
acquireSingeExtractorLink("Copy Link") { link ->
|
||||||
val serviceClipboard =
|
val serviceClipboard =
|
||||||
(requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?)
|
(requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?)
|
||||||
?: return@aquireSingeExtractorLink
|
?: return@acquireSingeExtractorLink
|
||||||
val clip = ClipData.newPlainText(link.name, link.url)
|
val clip = ClipData.newPlainText(link.name, link.url)
|
||||||
serviceClipboard.setPrimaryClip(clip)
|
serviceClipboard.setPrimaryClip(clip)
|
||||||
Toast.makeText(requireContext(), "Text Copied", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Text Copied", Toast.LENGTH_SHORT).show()
|
||||||
|
@ -558,7 +589,7 @@ class ResultFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ACTION_PLAY_EPISODE_IN_BROWSER -> {
|
ACTION_PLAY_EPISODE_IN_BROWSER -> {
|
||||||
aquireSingeExtractorLink("Play in Browser") { link ->
|
acquireSingeExtractorLink("Play in Browser") { link ->
|
||||||
val i = Intent(ACTION_VIEW)
|
val i = Intent(ACTION_VIEW)
|
||||||
i.data = Uri.parse(link.url)
|
i.data = Uri.parse(link.url)
|
||||||
startActivity(i)
|
startActivity(i)
|
||||||
|
@ -566,7 +597,7 @@ class ResultFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ACTION_CHROME_CAST_MIRROR -> {
|
ACTION_CHROME_CAST_MIRROR -> {
|
||||||
aquireSingeExtractorLink("Cast Mirror") { link ->
|
acquireSingeExtractorLink("Cast Mirror") { link ->
|
||||||
val mirrorIndex = currentLinks?.indexOf(link) ?: -1
|
val mirrorIndex = currentLinks?.indexOf(link) ?: -1
|
||||||
startChromecast(if (mirrorIndex == -1) 0 else mirrorIndex)
|
startChromecast(if (mirrorIndex == -1) 0 else mirrorIndex)
|
||||||
}
|
}
|
||||||
|
@ -657,15 +688,15 @@ class ResultFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ACTION_DOWNLOAD_EPISODE -> {
|
ACTION_DOWNLOAD_EPISODE -> {
|
||||||
startDownload(currentLinks ?: return@main)
|
startDownload(currentLinks ?: return@main, currentSubs)
|
||||||
}
|
}
|
||||||
|
|
||||||
ACTION_DOWNLOAD_MIRROR -> {
|
ACTION_DOWNLOAD_MIRROR -> {
|
||||||
aquireSingeExtractorLink(
|
acquireSingeExtractorLink(
|
||||||
(currentLinks ?: return@main).filter { !it.isM3u8 },
|
(currentLinks ?: return@main).filter { !it.isM3u8 },
|
||||||
"Download Mirror"
|
"Download Mirror"
|
||||||
) { link ->
|
) { link ->
|
||||||
startDownload(listOf(link))
|
startDownload(listOf(link), currentSubs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,12 +26,17 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||||
import com.lagradost.cloudstream3.utils.Event
|
import com.lagradost.cloudstream3.utils.Event
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
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.fixPaddingStatusbar
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||||
import kotlinx.android.synthetic.main.subtitle_settings.*
|
import kotlinx.android.synthetic.main.subtitle_settings.*
|
||||||
|
|
||||||
const val SUBTITLE_KEY = "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(
|
data class SaveCaptionStyle(
|
||||||
var foregroundColor: Int,
|
var foregroundColor: Int,
|
||||||
|
@ -111,6 +116,14 @@ class SubtitlesFragment : Fragment() {
|
||||||
val metrics: DisplayMetrics = Resources.getSystem().displayMetrics
|
val metrics: DisplayMetrics = Resources.getSystem().displayMetrics
|
||||||
return TypedValue.applyDimension(unit, size, metrics).toInt()
|
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>) {
|
private fun onColorSelected(stuff: Pair<Int, Int>) {
|
||||||
|
@ -296,10 +309,57 @@ class SubtitlesFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subs_font.setOnLongClickListener {
|
subs_font.setOnLongClickListener { textView ->
|
||||||
state.typeface = null
|
state.typeface = null
|
||||||
it.context.updateState()
|
textView.context.updateState()
|
||||||
Toast.makeText(it.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show()
|
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
|
return@setOnLongClickListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,15 +6,17 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
data class ExtractorLink(
|
data class ExtractorLink(
|
||||||
val source: String,
|
val source: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val url: String,
|
override val url: String,
|
||||||
val referer: String,
|
override val referer: String,
|
||||||
val quality: Int,
|
val quality: Int,
|
||||||
val isM3u8: Boolean = false,
|
val isM3u8: Boolean = false,
|
||||||
)
|
) : VideoDownloadManager.IDownloadableMinimum
|
||||||
|
|
||||||
fun ExtractorLink.getId(): Int {
|
data class ExtractorSubtitleLink(
|
||||||
return url.hashCode()
|
val name: String,
|
||||||
}
|
override val url: String,
|
||||||
|
override val referer: String,
|
||||||
|
) : VideoDownloadManager.IDownloadableMinimum
|
||||||
|
|
||||||
enum class Qualities(var value: Int) {
|
enum class Qualities(var value: Int) {
|
||||||
Unknown(0),
|
Unknown(0),
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.util.forEach
|
||||||
import androidx.core.view.marginLeft
|
import androidx.core.view.marginLeft
|
||||||
import androidx.core.view.marginRight
|
import androidx.core.view.marginRight
|
||||||
import androidx.core.view.marginTop
|
import androidx.core.view.marginTop
|
||||||
|
@ -15,20 +16,22 @@ object SingleSelectionHelper {
|
||||||
fun Context.showDialog(
|
fun Context.showDialog(
|
||||||
dialog: Dialog,
|
dialog: Dialog,
|
||||||
items: List<String>,
|
items: List<String>,
|
||||||
selectedIndex: Int,
|
selectedIndex: List<Int>,
|
||||||
name: String,
|
name: String,
|
||||||
showApply: Boolean,
|
showApply: Boolean,
|
||||||
callback: (Int) -> Unit,
|
isMultiSelect: Boolean,
|
||||||
|
callback: (List<Int>) -> Unit,
|
||||||
dismissCallback: () -> Unit
|
dismissCallback: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val realShowApply = showApply || isMultiSelect
|
||||||
val listView = dialog.findViewById<ListView>(R.id.listview1)!!
|
val listView = dialog.findViewById<ListView>(R.id.listview1)!!
|
||||||
val textView = dialog.findViewById<TextView>(R.id.text1)!!
|
val textView = dialog.findViewById<TextView>(R.id.text1)!!
|
||||||
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
|
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
|
||||||
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
|
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
|
||||||
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
|
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
|
||||||
|
|
||||||
applyHolder.visibility = if (showApply) View.VISIBLE else View.GONE
|
applyHolder.visibility = if (realShowApply) View.VISIBLE else View.GONE
|
||||||
if (!showApply) {
|
if (!realShowApply) {
|
||||||
val params = listView.layoutParams as LinearLayout.LayoutParams
|
val params = listView.layoutParams as LinearLayout.LayoutParams
|
||||||
params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0)
|
params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0)
|
||||||
listView.layoutParams = params
|
listView.layoutParams = params
|
||||||
|
@ -40,29 +43,45 @@ object SingleSelectionHelper {
|
||||||
arrayAdapter.addAll(items)
|
arrayAdapter.addAll(items)
|
||||||
|
|
||||||
listView.adapter = arrayAdapter
|
listView.adapter = arrayAdapter
|
||||||
|
if (isMultiSelect) {
|
||||||
|
listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
||||||
|
} else {
|
||||||
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||||
|
}
|
||||||
|
|
||||||
listView.setSelection(selectedIndex)
|
for (select in selectedIndex) {
|
||||||
listView.setItemChecked(selectedIndex, true)
|
listView.setItemChecked(select, true)
|
||||||
|
}
|
||||||
|
|
||||||
var currentIndex = selectedIndex
|
selectedIndex.minOrNull()?.let {
|
||||||
|
listView.setSelection(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1
|
||||||
|
|
||||||
dialog.setOnDismissListener {
|
dialog.setOnDismissListener {
|
||||||
dismissCallback.invoke()
|
dismissCallback.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
listView.setOnItemClickListener { _, _, which, _ ->
|
listView.setOnItemClickListener { _, _, which, _ ->
|
||||||
if (showApply) {
|
// lastSelectedIndex = which
|
||||||
currentIndex = which
|
if (realShowApply) {
|
||||||
|
if (!isMultiSelect) {
|
||||||
listView.setItemChecked(which, true)
|
listView.setItemChecked(which, true)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
callback.invoke(which)
|
callback.invoke(listOf(which))
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showApply) {
|
if (realShowApply) {
|
||||||
applyButton.setOnClickListener {
|
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()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
cancelButton.setOnClickListener {
|
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(
|
fun Context.showDialog(
|
||||||
items: List<String>,
|
items: List<String>,
|
||||||
selectedIndex: Int,
|
selectedIndex: Int,
|
||||||
|
@ -84,7 +118,16 @@ object SingleSelectionHelper {
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
dialog.show()
|
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(
|
fun Context.showBottomDialog(
|
||||||
|
@ -100,6 +143,15 @@ object SingleSelectionHelper {
|
||||||
builder.setContentView(R.layout.bottom_selection_dialog)
|
builder.setContentView(R.layout.bottom_selection_dialog)
|
||||||
|
|
||||||
builder.show()
|
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("Malayalam", "മലയാളം", "ml", "mal", "mal", "mal", ""),
|
||||||
Language639("Maltese", "Malti", "mt", "mlt", "mlt", "mlt", ""),
|
Language639("Maltese", "Malti", "mt", "mlt", "mlt", "mlt", ""),
|
||||||
Language639("Māori", "te reo Māori", "mi", "mri", "", "mri", ""),
|
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("Marshallese", "Kajin M̧ajeļ", "mh", "mah", "mah", "mah", ""),
|
||||||
Language639("Mongolian", "Монгол хэл", "mn", "mon", "mon", "mon", ""),
|
Language639("Mongolian", "Монгол хэл", "mn", "mon", "mon", "mon", ""),
|
||||||
Language639("Nauruan", "Dorerin Naoero", "na", "nau", "nau", "nau", ""),
|
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("Reunion Creole", "Kréol Rénioné", "rc", "rcf", "rcf", "rcf", ""),
|
||||||
Language639("Romanian", "limba română", "ro", "ron", "", "ron", ""),
|
Language639("Romanian", "limba română", "ro", "ron", "", "ron", ""),
|
||||||
Language639("Russian", "Русский", "ru", "rus", "rus", "rus", ""),
|
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("Sardinian", "sardu", "sc", "srd", "srd", "srd", ""),
|
||||||
Language639("Sindhi", "सिन्धी, سنڌي، سندھی", "sd", "snd", "snd", "snd", ""),
|
Language639("Sindhi", "सिन्धी, سنڌي، سندھی", "sd", "snd", "snd", "snd", ""),
|
||||||
Language639("Northern Sami", "Davvisámegiella", "se", "sme", "sme", "sme", ""),
|
Language639("Northern Sami", "Davvisámegiella", "se", "sme", "sme", "sme", ""),
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.RequiresApi
|
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.removeKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getExistingDownloadUriOrNullQ
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -35,6 +37,7 @@ import java.lang.Thread.sleep
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
|
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
|
||||||
const val DOWNLOAD_CHANNEL_NAME = "Downloads"
|
const val DOWNLOAD_CHANNEL_NAME = "Downloads"
|
||||||
|
@ -85,6 +88,15 @@ object VideoDownloadManager {
|
||||||
Stop,
|
Stop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IDownloadableMinimum {
|
||||||
|
val url: String
|
||||||
|
val referer: String
|
||||||
|
}
|
||||||
|
|
||||||
|
fun VideoDownloadManager.IDownloadableMinimum.getId(): Int {
|
||||||
|
return url.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
data class DownloadEpisodeMetadata(
|
data class DownloadEpisodeMetadata(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val mainName: String,
|
val mainName: String,
|
||||||
|
@ -126,7 +138,7 @@ object VideoDownloadManager {
|
||||||
|
|
||||||
private const val SUCCESS_DOWNLOAD_DONE = 1
|
private const val SUCCESS_DOWNLOAD_DONE = 1
|
||||||
private const val SUCCESS_STOPPED = 2
|
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_CREATE_FILE = -2
|
||||||
private const val ERROR_OPEN_FILE = -3
|
private const val ERROR_OPEN_FILE = -3
|
||||||
private const val ERROR_TOO_SMALL_CONNECTION = -4
|
private const val ERROR_TOO_SMALL_CONNECTION = -4
|
||||||
|
@ -361,6 +373,69 @@ object VideoDownloadManager {
|
||||||
return tempName.replace(" ", " ").trim(' ')
|
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)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? {
|
private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? {
|
||||||
try {
|
try {
|
||||||
|
@ -412,18 +487,24 @@ object VideoDownloadManager {
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadSingleEpisode(
|
data class CreateNotificationMetadata(
|
||||||
context: Context,
|
val type: DownloadType,
|
||||||
source: String?,
|
val bytesDownloaded: Long,
|
||||||
folder: String?,
|
val bytesTotal: Long,
|
||||||
ep: DownloadEpisodeMetadata,
|
)
|
||||||
link: ExtractorLink,
|
|
||||||
tryResume: Boolean = false,
|
|
||||||
): Int {
|
|
||||||
val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}")
|
|
||||||
|
|
||||||
|
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 relativePath = (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
|
||||||
val displayName = "$name.mp4"
|
val displayName = "$name.$extension"
|
||||||
|
|
||||||
val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
|
val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
|
||||||
var resume = tryResume
|
var resume = tryResume
|
||||||
|
@ -440,7 +521,9 @@ object VideoDownloadManager {
|
||||||
} else {
|
} else {
|
||||||
if (!File(normalPath).delete()) return ERROR_DELETING_FILE
|
if (!File(normalPath).delete()) return ERROR_DELETING_FILE
|
||||||
}
|
}
|
||||||
downloadDeleteEvent.invoke(ep.id)
|
parentId?.let {
|
||||||
|
downloadDeleteEvent.invoke(parentId)
|
||||||
|
}
|
||||||
return SUCCESS_STOPPED
|
return SUCCESS_STOPPED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -468,11 +551,18 @@ object VideoDownloadManager {
|
||||||
} else {
|
} else {
|
||||||
val contentUri =
|
val contentUri =
|
||||||
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
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 {
|
val newFile = ContentValues().apply {
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||||
put(MediaStore.MediaColumns.TITLE, name)
|
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)
|
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,9 +629,11 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
val bytesTotal = contentLength + resumeLength
|
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,
|
// Could use connection.contentType for mime types when creating the file,
|
||||||
// however file is already created and players don't go of file type
|
// however file is already created and players don't go of file type
|
||||||
|
@ -573,15 +665,18 @@ object VideoDownloadManager {
|
||||||
else -> DownloadType.IsDownloading
|
else -> DownloadType.IsDownloading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parentId?.let { id ->
|
||||||
try {
|
try {
|
||||||
downloadStatus[ep.id] = type
|
downloadStatus[id] = type
|
||||||
downloadStatusEvent.invoke(Pair(ep.id, type))
|
downloadStatusEvent.invoke(Pair(id, type))
|
||||||
downloadProgressEvent.invoke(Triple(ep.id, bytesDownloaded, bytesTotal))
|
downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// IDK MIGHT ERROR
|
// IDK MIGHT ERROR
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createNotification(
|
createNotificationCallback.invoke(CreateNotificationMetadata(type, bytesDownloaded, bytesTotal))
|
||||||
|
/*createNotification(
|
||||||
context,
|
context,
|
||||||
source,
|
source,
|
||||||
link.name,
|
link.name,
|
||||||
|
@ -589,12 +684,11 @@ object VideoDownloadManager {
|
||||||
type,
|
type,
|
||||||
bytesDownloaded,
|
bytesDownloaded,
|
||||||
bytesTotal
|
bytesTotal
|
||||||
)
|
)*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val downloadEventListener = { event: Pair<Int, DownloadActionType> ->
|
val downloadEventListener = { event: Pair<Int, DownloadActionType> ->
|
||||||
if (event.first == ep.id) {
|
if (event.first == parentId) {
|
||||||
when (event.second) {
|
when (event.second) {
|
||||||
DownloadActionType.Pause -> {
|
DownloadActionType.Pause -> {
|
||||||
isPaused = true; updateNotification()
|
isPaused = true; updateNotification()
|
||||||
|
@ -611,6 +705,7 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parentId != null)
|
||||||
downloadEvent += downloadEventListener
|
downloadEvent += downloadEventListener
|
||||||
|
|
||||||
// UPDATE DOWNLOAD NOTIFICATION
|
// UPDATE DOWNLOAD NOTIFICATION
|
||||||
|
@ -625,7 +720,6 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val id = ep.id
|
|
||||||
// THE REAL READ
|
// THE REAL READ
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -655,13 +749,16 @@ object VideoDownloadManager {
|
||||||
notificationCoroutine.cancel()
|
notificationCoroutine.cancel()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (parentId != null)
|
||||||
downloadEvent -= downloadEventListener
|
downloadEvent -= downloadEventListener
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
downloadStatus.remove(ep.id)
|
parentId?.let {
|
||||||
|
downloadStatus.remove(it)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// IDK MIGHT ERROR
|
// IDK MIGHT ERROR
|
||||||
}
|
}
|
||||||
|
@ -669,15 +766,15 @@ object VideoDownloadManager {
|
||||||
// RETURN MESSAGE
|
// RETURN MESSAGE
|
||||||
return when {
|
return when {
|
||||||
isFailed -> {
|
isFailed -> {
|
||||||
downloadProgressEvent.invoke(Triple(id, 0, 0))
|
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
|
||||||
ERROR_CONNECTION_ERROR
|
ERROR_CONNECTION_ERROR
|
||||||
}
|
}
|
||||||
isStopped -> {
|
isStopped -> {
|
||||||
downloadProgressEvent.invoke(Triple(id, 0, 0))
|
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
|
||||||
deleteFile()
|
deleteFile()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal))
|
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) }
|
||||||
isDone = true
|
isDone = true
|
||||||
updateNotification()
|
updateNotification()
|
||||||
SUCCESS_DOWNLOAD_DONE
|
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) {
|
private fun downloadCheck(context: Context) {
|
||||||
if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) {
|
if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) {
|
||||||
val pkg = downloadQueue.removeFirst()
|
val pkg = downloadQueue.removeFirst()
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
android:baselineAligned="false">
|
android:baselineAligned="false">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/sort_sources_holder"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
<TextView
|
<TextView
|
||||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
android:paddingStart="20dp"
|
||||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
android:paddingEnd="20dp"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_marginTop="20dp"
|
||||||
android:layout_marginBottom="10dp"
|
android:layout_marginBottom="10dp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
|
@ -78,6 +78,26 @@
|
||||||
android:text="@string/subs_subtitle_elevation"
|
android:text="@string/subs_subtitle_elevation"
|
||||||
style="@style/SettingsItem"
|
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
|
<LinearLayout
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
|
|
@ -88,4 +88,7 @@
|
||||||
<string name="manual_check_update_key">manual_check_update</string>
|
<string name="manual_check_update_key">manual_check_update</string>
|
||||||
|
|
||||||
<string name="prerelease_commit_hash">unknown_prerelease</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>
|
</resources>
|
Loading…
Reference in a new issue