mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
subs
This commit is contained in:
parent
6b27db036b
commit
3f8229756d
17 changed files with 450 additions and 101 deletions
|
@ -113,6 +113,8 @@ abstract class MainAPI {
|
|||
}
|
||||
}
|
||||
|
||||
class ErrorLoadingException(message: String? = null) : Exception(message)
|
||||
|
||||
fun parseRating(ratingString: String?): Int? {
|
||||
if (ratingString == null) return null
|
||||
val floatRating = ratingString.toFloatOrNull() ?: return null
|
||||
|
@ -149,6 +151,19 @@ fun sortSubs(urls: List<SubtitleFile>): List<SubtitleFile> {
|
|||
}
|
||||
}
|
||||
|
||||
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
|
||||
fun imdbUrlToId(url: String): String {
|
||||
return url
|
||||
.removePrefix("https://www.imdb.com/title/")
|
||||
.removePrefix("https://imdb.com/title/tt2861424/")
|
||||
.replace("/", "")
|
||||
}
|
||||
|
||||
fun imdbUrlToIdNullable(url: String?): String? {
|
||||
if(url == null) return null
|
||||
return imdbUrlToId(url)
|
||||
}
|
||||
|
||||
enum class ShowStatus {
|
||||
Completed,
|
||||
Ongoing,
|
||||
|
@ -301,7 +316,7 @@ data class MovieLoadResponse(
|
|||
override val year: Int?,
|
||||
override val plot: String?,
|
||||
|
||||
val imdbUrl: String?,
|
||||
val imdbId: String?,
|
||||
override val rating: Int? = null,
|
||||
override val tags: ArrayList<String>? = null,
|
||||
override val duration: String? = null,
|
||||
|
@ -331,7 +346,7 @@ data class TvSeriesLoadResponse(
|
|||
override val plot: String?,
|
||||
|
||||
val showStatus: ShowStatus?,
|
||||
val imdbUrl: String?,
|
||||
val imdbId: String?,
|
||||
override val rating: Int? = null,
|
||||
override val tags: ArrayList<String>? = null,
|
||||
override val duration: String? = null,
|
||||
|
|
|
@ -31,10 +31,8 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey
|
|||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.createISO
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.fragment_result.*
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
const val VLC_PACKAGE = "org.videolan.vlc"
|
||||
const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result"
|
||||
|
@ -58,7 +56,7 @@ class MainActivity : AppCompatActivity() {
|
|||
return appViewModelStore
|
||||
}*/
|
||||
companion object {
|
||||
var isInPlayer: Boolean = false
|
||||
var canEnterPipMode: Boolean = false
|
||||
var canShowPipMode: Boolean = false
|
||||
var isInPIPMode: Boolean = false
|
||||
|
||||
|
@ -67,7 +65,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun enterPIPMode() {
|
||||
if (!shouldShowPIPMode(isInPlayer) || !canShowPipMode) return
|
||||
if (!shouldShowPIPMode(canEnterPipMode) || !canShowPipMode) return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
enterPictureInPictureMode(PictureInPictureParams.Builder().build())
|
||||
|
@ -83,7 +81,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
if (isInPlayer && canShowPipMode) {
|
||||
if (canEnterPipMode && canShowPipMode) {
|
||||
enterPIPMode()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ class MeloMovieProvider : MainAPI() {
|
|||
val plot = document.selectFirst("div.col-lg-12 > p").text()
|
||||
|
||||
if (type == 1) { // MOVIE
|
||||
val serialize = document.selectFirst("table.accordion__list")
|
||||
val serialize = document.selectFirst("table.accordion__list") ?: throw ErrorLoadingException("No links found")
|
||||
return MovieLoadResponse(
|
||||
title,
|
||||
url,
|
||||
|
@ -142,11 +142,11 @@ class MeloMovieProvider : MainAPI() {
|
|||
poster,
|
||||
year,
|
||||
plot,
|
||||
imdbUrl
|
||||
imdbUrlToIdNullable(imdbUrl)
|
||||
)
|
||||
} else if (type == 2) {
|
||||
val episodes = ArrayList<TvSeriesEpisode>()
|
||||
val seasons = document.select("div.accordion__card")
|
||||
val seasons = document.select("div.accordion__card") ?: throw ErrorLoadingException("No episodes found")
|
||||
for (s in seasons) {
|
||||
val season =
|
||||
s.selectFirst("> div.card-header > button > span").text().replace("Season: ", "").toIntOrNull()
|
||||
|
@ -154,7 +154,7 @@ class MeloMovieProvider : MainAPI() {
|
|||
for (e in localEpisodes) {
|
||||
val episode =
|
||||
e.selectFirst("> div.card-header > button > span").text().replace("Episode: ", "").toIntOrNull()
|
||||
val links = e.selectFirst("> div.collapse > div > table.accordion__list")
|
||||
val links = e.selectFirst("> div.collapse > div > table.accordion__list") ?: continue
|
||||
val data = serializeData(links)
|
||||
episodes.add(TvSeriesEpisode(null, season, episode, data))
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ class MeloMovieProvider : MainAPI() {
|
|||
year,
|
||||
plot,
|
||||
null,
|
||||
imdbUrl
|
||||
imdbUrlToIdNullable(imdbUrl)
|
||||
)
|
||||
}
|
||||
return null
|
||||
|
|
|
@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.movieproviders
|
|||
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.imdbUrlToId
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
|
@ -255,7 +254,7 @@ class TrailersToProvider : MainAPI() {
|
|||
year,
|
||||
descript,
|
||||
null,
|
||||
imdbUrl,
|
||||
imdbUrlToIdNullable(imdbUrl),
|
||||
rating,
|
||||
tags,
|
||||
duration,
|
||||
|
@ -283,7 +282,7 @@ class TrailersToProvider : MainAPI() {
|
|||
poster,
|
||||
year,
|
||||
descript,
|
||||
imdbUrl,
|
||||
imdbUrlToIdNullable(imdbUrl),
|
||||
rating,
|
||||
tags,
|
||||
duration,
|
||||
|
|
|
@ -5,7 +5,7 @@ import androidx.lifecycle.LifecycleOwner
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import com.bumptech.glide.load.HttpException
|
||||
import com.lagradost.cloudstream3.ui.ErrorLoadingException
|
||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.SocketTimeoutException
|
||||
|
@ -30,7 +30,8 @@ sealed class Resource<out T> {
|
|||
val errorResponse: Any?, //ResponseBody
|
||||
val errorString: String,
|
||||
) : Resource<Nothing>()
|
||||
data class Loading(val url : String? = null) : Resource<Nothing>()
|
||||
|
||||
data class Loading(val url: String? = null) : Resource<Nothing>()
|
||||
}
|
||||
|
||||
fun logError(throwable: Throwable) {
|
||||
|
@ -41,7 +42,7 @@ fun logError(throwable: Throwable) {
|
|||
Log.d("ApiError", "-------------------------------------------------------------------")
|
||||
}
|
||||
|
||||
fun<T> normalSafeApiCall(apiCall : () -> T) : T? {
|
||||
fun <T> normalSafeApiCall(apiCall: () -> T): T? {
|
||||
return try {
|
||||
apiCall.invoke()
|
||||
} catch (throwable: Throwable) {
|
||||
|
@ -69,7 +70,7 @@ suspend fun <T> safeApiCall(
|
|||
Resource.Failure(true, null, null, "Cannot connect to server, try again later.")
|
||||
}
|
||||
is ErrorLoadingException -> {
|
||||
Resource.Failure(true, null, null, "Error loading, try again later.")
|
||||
Resource.Failure(true, null, null, throwable.message ?: "Error loading, try again later.")
|
||||
}
|
||||
else -> {
|
||||
val stackTraceMsg = throwable.localizedMessage + "\n\n" + throwable.stackTrace.joinToString(
|
||||
|
|
|
@ -6,8 +6,6 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
|||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
|
||||
class ErrorLoadingException(message: String) : Exception(message)
|
||||
|
||||
class APIRepository(val api: MainAPI) {
|
||||
val name: String get() = api.name
|
||||
val mainUrl: String get() = api.mainUrl
|
||||
|
@ -15,25 +13,25 @@ class APIRepository(val api: MainAPI) {
|
|||
suspend fun load(url: String): Resource<LoadResponse> {
|
||||
return safeApiCall {
|
||||
// remove suffix for some slugs to handle correctly
|
||||
api.load(url.removeSuffix("/")) ?: throw ErrorLoadingException("Error Loading")
|
||||
api.load(url.removeSuffix("/")) ?: throw ErrorLoadingException()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun search(query: String): Resource<ArrayList<SearchResponse>> {
|
||||
return safeApiCall {
|
||||
api.search(query) ?: throw ErrorLoadingException("Error Loading")
|
||||
api.search(query) ?: throw ErrorLoadingException()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun quickSearch(query: String): Resource<ArrayList<SearchResponse>> {
|
||||
return safeApiCall {
|
||||
api.quickSearch(query) ?: throw ErrorLoadingException("Error Loading")
|
||||
api.quickSearch(query) ?: throw ErrorLoadingException()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMainPage(): Resource<HomePageResponse> {
|
||||
return safeApiCall {
|
||||
api.getMainPage() ?: throw ErrorLoadingException("Error Loading")
|
||||
api.getMainPage() ?: throw ErrorLoadingException()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,8 +23,6 @@ import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActi
|
|||
import com.lagradost.cloudstream3.APIHolder.getApiFromName
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.sortUrls
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks
|
||||
|
@ -97,7 +95,6 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
// lateinit var dialog: AlertDialog
|
||||
val holder = getCurrentMetaData()
|
||||
|
||||
|
||||
if (holder != null) {
|
||||
val items = holder.currentLinks
|
||||
if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) {
|
||||
|
@ -251,7 +248,6 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
) VISIBLE else INVISIBLE
|
||||
try {
|
||||
if (meta != null && meta.episodes.size > meta.currentEpisodeIndex + 1) {
|
||||
|
||||
val currentIdIndex = remoteMediaClient?.getItemIndex() ?: return
|
||||
val itemCount = remoteMediaClient?.mediaQueue?.itemCount
|
||||
|
||||
|
@ -264,8 +260,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
val links = ArrayList<ExtractorLink>()
|
||||
val subs = ArrayList<SubtitleFile>()
|
||||
|
||||
val res = safeApiCall {
|
||||
getApiFromName(meta.apiName).loadLinks(epData.data, true, { subtitleFile ->
|
||||
val isSuccessful =
|
||||
APIRepository(getApiFromName(meta.apiName)).loadLinks(epData.data, true, { subtitleFile ->
|
||||
if (!subs.any { it.url == subtitleFile.url }) {
|
||||
subs.add(subtitleFile)
|
||||
}
|
||||
|
@ -274,9 +270,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
links.add(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res is Resource.Success) {
|
||||
if (isSuccessful) {
|
||||
val sorted = sortUrls(links)
|
||||
if (sorted.isNotEmpty()) {
|
||||
val jsonCopy = meta.copy(
|
||||
|
|
|
@ -12,6 +12,7 @@ import android.content.pm.ActivityInfo
|
|||
import android.content.res.Resources
|
||||
import android.database.ContentObserver
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Icon
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
|
@ -25,8 +26,7 @@ import android.view.animation.AccelerateInterpolator
|
|||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import android.widget.*
|
||||
import android.widget.Toast.LENGTH_SHORT
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -45,6 +45,8 @@ import com.google.android.exoplayer2.C.TIME_UNSET
|
|||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
import com.google.android.exoplayer2.ui.CaptionStyleCompat
|
||||
import com.google.android.exoplayer2.ui.SubtitleView
|
||||
import com.google.android.exoplayer2.upstream.DataSource
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
||||
|
@ -53,17 +55,11 @@ import com.google.android.exoplayer2.util.Util
|
|||
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.cast.framework.CastState
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.isInPIPMode
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.isInPlayer
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.canEnterPipMode
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getNavigationBarHeight
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
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.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.mvvm.observeDirectly
|
||||
|
@ -79,7 +75,13 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey
|
|||
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.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getNavigationBarHeight
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
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 kotlinx.android.synthetic.main.fragment_player.*
|
||||
|
@ -791,6 +793,14 @@ class PlayerFragment : Fragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val subs = player_view.findViewById<SubtitleView>(R.id.exo_subtitles)
|
||||
subs.setStyle(
|
||||
CaptionStyleCompat(
|
||||
Color.WHITE, Color.TRANSPARENT, Color.TRANSPARENT, CaptionStyleCompat.EDGE_TYPE_OUTLINE, Color.BLACK,
|
||||
Typeface.SANS_SERIF
|
||||
)
|
||||
)
|
||||
|
||||
settingsManager = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
swipeEnabled = settingsManager.getBoolean("swipe_enabled", true)
|
||||
swipeVerticalEnabled = settingsManager.getBoolean("swipe_vertical_enabled", true)
|
||||
|
@ -800,8 +810,6 @@ class PlayerFragment : Fragment() {
|
|||
|
||||
brightness_overlay?.alpha = context?.getKey(VIDEO_PLAYER_BRIGHTNESS, 0f) ?: 0f
|
||||
|
||||
isInPlayer = true // NEED REFERENCE TO MAIN ACTIVITY FOR PIP
|
||||
|
||||
navigationBarHeight = requireContext().getNavigationBarHeight()
|
||||
statusBarHeight = requireContext().getStatusBarHeight()
|
||||
|
||||
|
@ -898,9 +906,9 @@ class PlayerFragment : Fragment() {
|
|||
}
|
||||
|
||||
sources_btt.visibility =
|
||||
if (isDownloadedFile) View.GONE else View.VISIBLE
|
||||
if (isDownloadedFile) GONE else VISIBLE
|
||||
player_media_route_button.visibility =
|
||||
if (isDownloadedFile) View.GONE else View.VISIBLE
|
||||
if (isDownloadedFile) GONE else VISIBLE
|
||||
if (savedInstanceState != null) {
|
||||
currentWindow = savedInstanceState.getInt(STATE_RESUME_WINDOW)
|
||||
playbackPosition = savedInstanceState.getLong(STATE_RESUME_POSITION)
|
||||
|
@ -1138,27 +1146,126 @@ class PlayerFragment : Fragment() {
|
|||
lateinit var dialog: AlertDialog
|
||||
getUrls()?.let { it1 ->
|
||||
sortUrls(it1).let { sources ->
|
||||
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 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.pick_source_apply)!!
|
||||
val cancelButton = sourceDialog.findViewById<MaterialButton>(R.id.pick_source_cancel)!!
|
||||
|
||||
val startSource = sources.indexOf(getCurrentUrl())
|
||||
var sourceIndex = startSource
|
||||
val startSubtitle = currentSubtitles.indexOf(preferredSubtitles) + 1
|
||||
var subtitleIndex = startSubtitle
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
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()
|
||||
dialog.dismiss()
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
dialog = builder.create()
|
||||
dialog.show()*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1192,6 +1299,18 @@ class PlayerFragment : Fragment() {
|
|||
// initPlayer()
|
||||
}
|
||||
|
||||
private fun getRendererIndex(trackIndex: Int): Int? {
|
||||
if (!this::exoPlayer.isInitialized) return null
|
||||
|
||||
for (renderIndex in 0 until exoPlayer.rendererCount) {
|
||||
if (exoPlayer.getRendererType(renderIndex) == renderIndex) {
|
||||
return renderIndex
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getCurrentUrl(): ExtractorLink? {
|
||||
val urls = getUrls() ?: return null
|
||||
for (i in urls) {
|
||||
|
@ -1201,12 +1320,6 @@ class PlayerFragment : Fragment() {
|
|||
}
|
||||
|
||||
return null
|
||||
/*ExtractorLink("",
|
||||
"TEST",
|
||||
"https://v6.4animu.me/Overlord/Overlord-Episode-01-1080p.mp4",
|
||||
//"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
||||
"",
|
||||
0)*/
|
||||
}
|
||||
|
||||
private fun getUrls(): List<ExtractorLink>? {
|
||||
|
@ -1306,7 +1419,7 @@ class PlayerFragment : Fragment() {
|
|||
savePos()
|
||||
|
||||
super.onDestroy()
|
||||
isInPlayer = false
|
||||
canEnterPipMode = false
|
||||
|
||||
savePositionInPlayer()
|
||||
safeReleasePlayer()
|
||||
|
@ -1377,6 +1490,18 @@ class PlayerFragment : Fragment() {
|
|||
|
||||
private val updateProgressAction = Runnable { updateProgressBar() }*/
|
||||
|
||||
private fun String.toSubtitleMimeType(): String {
|
||||
return when {
|
||||
endsWith("vtt", true) -> MimeTypes.TEXT_VTT
|
||||
endsWith("srt", true) -> MimeTypes.APPLICATION_SUBRIP
|
||||
endsWith("xml", true) || endsWith("ttml", true) -> MimeTypes.APPLICATION_TTML
|
||||
else -> MimeTypes.TEXT_VTT
|
||||
}
|
||||
}
|
||||
|
||||
var activeSubtitles: List<String> = listOf()
|
||||
var preferredSubtitles: String = ""
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun initPlayer(currentUrl: ExtractorLink?, uri: String? = null) {
|
||||
if (currentUrl == null && uri == null) return
|
||||
|
@ -1384,7 +1509,6 @@ class PlayerFragment : Fragment() {
|
|||
hasUsedFirstRender = false
|
||||
|
||||
try {
|
||||
if (!isInPlayer) return
|
||||
if (this::exoPlayer.isInitialized) {
|
||||
savePos()
|
||||
exoPlayer.release()
|
||||
|
@ -1446,11 +1570,35 @@ class PlayerFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
val subs = getSubs()
|
||||
if (subs != null) {
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
val trackSelector = DefaultTrackSelector(requireContext())
|
||||
// Disable subtitles
|
||||
trackSelector.parameters = DefaultTrackSelector.ParametersBuilder(requireContext())
|
||||
.setRendererDisabled(C.TRACK_TYPE_VIDEO, true)
|
||||
// .setRendererDisabled(C.TRACK_TYPE_VIDEO, true)
|
||||
.setRendererDisabled(C.TRACK_TYPE_TEXT, true)
|
||||
.setDisabledTextTrackSelectionFlags(C.TRACK_TYPE_TEXT)
|
||||
.clearSelectionOverrides()
|
||||
|
@ -1530,6 +1678,18 @@ class PlayerFragment : Fragment() {
|
|||
|
||||
*/
|
||||
|
||||
/*exoPlayer.addTextOutput { list ->
|
||||
if (list.size == 0) return@addTextOutput
|
||||
|
||||
val textBuilder = StringBuilder()
|
||||
for (cue in list) {
|
||||
textBuilder.append(cue.text).append("\n")
|
||||
}
|
||||
val subtitleText = if (textBuilder.isNotEmpty())
|
||||
textBuilder.substring(0, textBuilder.length - 1)
|
||||
else
|
||||
textBuilder.toString()
|
||||
}*/
|
||||
|
||||
//https://stackoverflow.com/questions/47731779/detect-pause-resume-in-exoplayer
|
||||
exoPlayer.addListener(object : Player.Listener {
|
||||
|
@ -1573,6 +1733,7 @@ class PlayerFragment : Fragment() {
|
|||
}
|
||||
|
||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||
canEnterPipMode = exoPlayer.isPlaying
|
||||
updatePIPModeActions()
|
||||
if (activity == null) return
|
||||
if (playWhenReady) {
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package com.lagradost.cloudstream3.ui.result
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -174,6 +176,15 @@ class EpisodeAdapter(
|
|||
episodeDescript?.visibility = View.GONE
|
||||
}
|
||||
|
||||
episodePoster?.setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
}
|
||||
|
||||
episodePoster?.setOnLongClickListener {
|
||||
Toast.makeText(it.context, R.string.play_episode_toast, Toast.LENGTH_SHORT).show()
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
episodeHolder.setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
}
|
||||
|
@ -197,7 +208,15 @@ class EpisodeAdapter(
|
|||
downloadButton.setUpButton(
|
||||
downloadInfo?.fileLength, downloadInfo?.totalBytes, episodeDownloadBar, episodeDownloadImage, null,
|
||||
VideoDownloadHelper.DownloadEpisodeCached(
|
||||
card.name, card.poster, card.episode, card.season, card.id, 0, card.rating, card.descript, System.currentTimeMillis(),
|
||||
card.name,
|
||||
card.poster,
|
||||
card.episode,
|
||||
card.season,
|
||||
card.id,
|
||||
0,
|
||||
card.rating,
|
||||
card.descript,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
) {
|
||||
if (it.action == DOWNLOAD_ACTION_DOWNLOAD) {
|
||||
|
|
|
@ -155,12 +155,4 @@ object AppUtils {
|
|||
}
|
||||
return currentAudioFocusRequest
|
||||
}
|
||||
|
||||
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
|
||||
fun imdbUrlToId(url: String): String {
|
||||
return url
|
||||
.removePrefix("https://www.imdb.com/title/")
|
||||
.removePrefix("https://imdb.com/title/tt2861424/")
|
||||
.replace("/", "")
|
||||
}
|
||||
}
|
5
app/src/main/res/color/check_selection_color.xml
Normal file
5
app/src/main/res/color/check_selection_color.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_activated="true" android:color="?attr/textColor"/>
|
||||
<item android:color="@color/transparent"/>
|
||||
</selector>
|
5
app/src/main/res/color/text_selection_color.xml
Normal file
5
app/src/main/res/color/text_selection_color.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_activated="true" android:color="?attr/textColor"/>
|
||||
<item android:color="?attr/grayTextColor"/>
|
||||
</selector>
|
|
@ -1,17 +1,19 @@
|
|||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/player_holder"
|
||||
android:screenOrientation="landscape"
|
||||
tools:orientation="vertical"
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/player_holder"
|
||||
android:screenOrientation="landscape"
|
||||
tools:orientation="vertical"
|
||||
>
|
||||
<View android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/shadow_overlay"
|
||||
android:background="@color/black_overlay"
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/shadow_overlay"
|
||||
android:background="@color/black_overlay"
|
||||
/>
|
||||
<!--
|
||||
<LinearLayout android:layout_width="match_parent"
|
||||
|
@ -383,7 +385,7 @@
|
|||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="normal"/>
|
||||
<!--app:buffered_color="@color/videoCache"-->
|
||||
<!--app:buffered_color="@color/videoCache"-->
|
||||
<com.google.android.exoplayer2.ui.DefaultTimeBar
|
||||
android:id="@id/exo_progress"
|
||||
android:layout_width="0dp"
|
||||
|
@ -471,7 +473,8 @@
|
|||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
<LinearLayout android:id="@+id/lock_holder" android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="match_parent">
|
||||
<LinearLayout android:id="@+id/lock_holder" android:orientation="horizontal"
|
||||
android:layout_width="wrap_content" android:layout_height="match_parent">
|
||||
<androidx.cardview.widget.CardView
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
131
app/src/main/res/layout/player_select_source_and_subs.xml
Normal file
131
app/src/main/res/layout/player_select_source_and_subs.xml
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:background="@null"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="50">
|
||||
<TextView
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/pick_source"
|
||||
android:textSize="20sp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_height="wrap_content">
|
||||
</TextView>
|
||||
<ListView
|
||||
android:layout_marginTop="-10dp"
|
||||
android:paddingTop="10dp"
|
||||
android:id="@+id/sort_providers"
|
||||
android:background="?attr/bitDarkerGrayBackground"
|
||||
tools:listitem="@layout/sort_bottom_single_choice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
/>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/sort_subtitles_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="50">
|
||||
<TextView
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/pick_subtitle"
|
||||
android:textSize="20sp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
</TextView>
|
||||
<ListView
|
||||
android:layout_marginTop="-10dp"
|
||||
android:paddingTop="10dp"
|
||||
android:id="@+id/sort_subtitles"
|
||||
android:background="?attr/bitDarkerGrayBackground"
|
||||
tools:listitem="@layout/sort_bottom_single_choice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_height="match_parent">
|
||||
</ListView>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="bottom|end"
|
||||
android:layout_marginTop="-60dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_height="50dp"
|
||||
android:layout_margin="5dp"
|
||||
|
||||
android:visibility="visible"
|
||||
android:textStyle="bold"
|
||||
app:rippleColor="?attr/grayBackground"
|
||||
android:textColor="?attr/grayBackground"
|
||||
app:iconTint="?attr/grayBackground"
|
||||
android:textAllCaps="false"
|
||||
app:iconGravity="textStart"
|
||||
app:strokeColor="?attr/grayBackground"
|
||||
app:backgroundTint="?attr/textColor"
|
||||
|
||||
app:iconSize="20dp"
|
||||
android:text="@string/sort_apply"
|
||||
android:id="@+id/pick_source_apply"
|
||||
android:textSize="15sp"
|
||||
app:cornerRadius="5dp"
|
||||
android:layout_width="wrap_content"
|
||||
>
|
||||
</com.google.android.material.button.MaterialButton>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_height="50dp"
|
||||
android:layout_margin="5dp"
|
||||
|
||||
app:iconGravity="textStart"
|
||||
app:strokeColor="?attr/textColor"
|
||||
android:backgroundTint="?attr/grayBackground"
|
||||
app:rippleColor="?attr/textColor"
|
||||
android:textColor="?attr/textColor"
|
||||
app:iconTint="?attr/textColor"
|
||||
android:textAllCaps="false"
|
||||
android:textStyle="bold"
|
||||
|
||||
app:iconSize="20dp"
|
||||
android:text="@string/sort_cancel"
|
||||
android:id="@+id/pick_source_cancel"
|
||||
android:textSize="15sp"
|
||||
app:cornerRadius="5dp"
|
||||
android:layout_width="wrap_content"
|
||||
>
|
||||
</com.google.android.material.button.MaterialButton>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
|
@ -22,11 +22,11 @@
|
|||
android:layout_height="wrap_content">
|
||||
<!--app:cardCornerRadius="@dimen/roundedImageRadius"-->
|
||||
<androidx.cardview.widget.CardView
|
||||
|
||||
android:layout_width="126dp"
|
||||
android:layout_height="72dp"
|
||||
>
|
||||
<ImageView
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:id="@+id/episode_poster"
|
||||
tools:src="@drawable/example_poster"
|
||||
android:scaleType="centerCrop"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<CheckedTextView
|
||||
<!--<CheckedTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -11,3 +11,27 @@
|
|||
android:checkMark="?android:attr/listChoiceIndicatorSingle"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"/>
|
||||
-->
|
||||
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/AppTextViewStyle"
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="@color/text_selection_color"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="7dip"
|
||||
tools:text="TEST"
|
||||
android:checkMark="?android:attr/listChoiceIndicatorSingle"
|
||||
android:ellipsize="marquee"
|
||||
android:drawableStart="@drawable/ic_baseline_check_24"
|
||||
android:drawableTint="@color/check_selection_color"
|
||||
tools:drawableTint="?attr/textColor"
|
||||
android:drawablePadding="20dp"
|
||||
/>
|
||||
|
|
|
@ -66,4 +66,7 @@
|
|||
<string name="filter_bookmarks">Filter Bookmarks</string>
|
||||
<string name="error_bookmarks_text">Bookmarks</string>
|
||||
<string name="action_remove_from_bookmarks">Remove</string>
|
||||
<string name="play_episode_toast">Play Episode</string>
|
||||
<string name="sort_apply">Apply</string>
|
||||
<string name="sort_cancel">Cancel</string>
|
||||
</resources>
|
Loading…
Reference in a new issue