mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge pull request #2 from reduplicated/newResultViewModel
New result view model
This commit is contained in:
commit
cd9bdb8ba7
54 changed files with 2750 additions and 2400 deletions
|
@ -198,7 +198,7 @@ object APIHolder {
|
|||
return null
|
||||
}
|
||||
|
||||
fun getLoadResponseIdFromUrl(url: String, apiName: String): Int {
|
||||
private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int {
|
||||
return url.replace(getApiFromName(apiName).mainUrl, "").replace("/", "").hashCode()
|
||||
}
|
||||
|
||||
|
@ -645,6 +645,7 @@ enum class ShowStatus {
|
|||
}
|
||||
|
||||
enum class DubStatus(val id: Int) {
|
||||
None(-1),
|
||||
Dubbed(1),
|
||||
Subbed(0),
|
||||
}
|
||||
|
@ -979,6 +980,10 @@ interface LoadResponse {
|
|||
private val aniListIdPrefix = aniListApi.idPrefix
|
||||
var isTrailersEnabled = true
|
||||
|
||||
fun LoadResponse.isMovie() : Boolean {
|
||||
return this.type.isMovieType()
|
||||
}
|
||||
|
||||
@JvmName("addActorNames")
|
||||
fun LoadResponse.addActors(actors: List<String>?) {
|
||||
this.actors = actors?.map { ActorData(Actor(it)) }
|
||||
|
@ -1119,6 +1124,7 @@ data class NextAiring(
|
|||
data class SeasonData(
|
||||
val season: Int,
|
||||
val name: String? = null,
|
||||
val displaySeason : Int? = null, // will use season if null
|
||||
)
|
||||
|
||||
interface EpisodeResponse {
|
||||
|
|
|
@ -332,6 +332,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
if (str.contains(appString)) {
|
||||
for (api in OAuth2Apis) {
|
||||
if (str.contains("/${api.redirectUrl}")) {
|
||||
val activity = this
|
||||
ioSafe {
|
||||
Log.i(TAG, "handleAppIntent $str")
|
||||
val isSuccessful = api.handleRedirect(str)
|
||||
|
@ -342,10 +343,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
Log.i(TAG, "failed to authenticate ${api.name}")
|
||||
}
|
||||
|
||||
this.runOnUiThread {
|
||||
activity.runOnUiThread {
|
||||
try {
|
||||
showToast(
|
||||
this,
|
||||
activity,
|
||||
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
|
||||
api.name
|
||||
)
|
||||
|
|
|
@ -54,7 +54,7 @@ class GogoanimeProvider : MainAPI() {
|
|||
secretKeyString: String,
|
||||
encrypt: Boolean = true
|
||||
): String {
|
||||
println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string")
|
||||
//println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string")
|
||||
val ivParameterSpec = IvParameterSpec(iv.toByteArray())
|
||||
val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES")
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
|
|
|
@ -51,6 +51,32 @@ fun <T> LifecycleOwner.observeDirectly(liveData: LiveData<T>, action: (t: T) ->
|
|||
action(currentValue)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> some(value: T?): Some<T> {
|
||||
return if (value == null) {
|
||||
Some.None
|
||||
} else {
|
||||
Some.Success(value)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Some<out T> {
|
||||
data class Success<out T>(val value: T) : Some<T>()
|
||||
object None : Some<Nothing>()
|
||||
|
||||
override fun toString(): String {
|
||||
return when(this) {
|
||||
is None -> "None"
|
||||
is Success -> "Some(${value.toString()})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ResourceSome<out T> {
|
||||
data class Success<out T>(val value: T) : ResourceSome<T>()
|
||||
object None : ResourceSome<Nothing>()
|
||||
data class Loading(val data: Any? = null) : ResourceSome<Nothing>()
|
||||
}
|
||||
|
||||
sealed class Resource<out T> {
|
||||
data class Success<out T>(val value: T) : Resource<T>()
|
||||
data class Failure(
|
||||
|
|
|
@ -31,6 +31,8 @@ class APIRepository(val api: MainAPI) {
|
|||
val mainUrl = api.mainUrl
|
||||
val mainPage = api.mainPage
|
||||
val hasQuickSearch = api.hasQuickSearch
|
||||
val vpnStatus = api.vpnStatus
|
||||
val providerType = api.providerType
|
||||
|
||||
suspend fun load(url: String): Resource<LoadResponse> {
|
||||
return safeApiCall {
|
||||
|
|
|
@ -18,7 +18,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
|||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
|
||||
object DownloadButtonSetup {
|
||||
fun handleDownloadClick(activity: Activity?, headerName: String?, click: DownloadClickEvent) {
|
||||
fun handleDownloadClick(activity: Activity?, click: DownloadClickEvent) {
|
||||
val id = click.data.id
|
||||
if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return
|
||||
when (click.action) {
|
||||
|
|
|
@ -84,7 +84,7 @@ class DownloadChildFragment : Fragment() {
|
|||
DownloadChildAdapter(
|
||||
ArrayList(),
|
||||
) { click ->
|
||||
handleDownloadClick(activity, name, click)
|
||||
handleDownloadClick(activity, click)
|
||||
}
|
||||
|
||||
downloadDeleteEventListener = { id: Int ->
|
||||
|
|
|
@ -153,7 +153,7 @@ class DownloadFragment : Fragment() {
|
|||
},
|
||||
{ downloadClickEvent ->
|
||||
if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter
|
||||
handleDownloadClick(activity, downloadClickEvent.data.name, downloadClickEvent)
|
||||
handleDownloadClick(activity, downloadClickEvent)
|
||||
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
|
||||
context?.let { ctx ->
|
||||
downloadsViewModel.updateList(ctx)
|
||||
|
|
|
@ -125,6 +125,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
private fun loadExtractorJob(extractorLink: ExtractorLink?) {
|
||||
currentVerifyLink?.cancel()
|
||||
|
||||
extractorLink?.let {
|
||||
currentVerifyLink = ioSafe {
|
||||
if (it.extractorData != null) {
|
||||
|
@ -488,7 +489,9 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
.setView(R.layout.player_select_source_and_subs)
|
||||
|
||||
val sourceDialog = sourceBuilder.create()
|
||||
|
||||
selectSourceDialog = sourceDialog
|
||||
|
||||
sourceDialog.show()
|
||||
val providerList = sourceDialog.sort_providers
|
||||
val subtitleList = sourceDialog.sort_subtitles
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.annotation.LayoutRes
|
|||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.lagradost.cloudstream3.R
|
||||
|
@ -56,7 +57,7 @@ const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
|
|||
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
|
||||
|
||||
class EpisodeAdapter(
|
||||
var cardList: List<ResultEpisode>,
|
||||
private var cardList: MutableList<ResultEpisode>,
|
||||
private val hasDownloadSupport: Boolean,
|
||||
private val clickCallback: (EpisodeClickEvent) -> Unit,
|
||||
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
|
||||
|
@ -92,13 +93,15 @@ class EpisodeAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
private var layout: Int = 0
|
||||
fun updateLayout() {
|
||||
// layout =
|
||||
// if (cardList.filter { it.poster != null }.size >= cardList.size / 2f) // If over half has posters then use the large layout
|
||||
// R.layout.result_episode_large
|
||||
// else R.layout.result_episode
|
||||
fun updateList(newList: List<ResultEpisode>) {
|
||||
val diffResult = DiffUtil.calculateDiff(
|
||||
ResultDiffCallback(this.cardList, newList)
|
||||
)
|
||||
|
||||
cardList.clear()
|
||||
cardList.addAll(newList)
|
||||
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
@ -263,3 +266,19 @@ class EpisodeAdapter(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ResultDiffCallback(
|
||||
private val oldList: List<ResultEpisode>,
|
||||
private val newList: List<ResultEpisode>
|
||||
) :
|
||||
DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition].id == newList[newItemPosition].id
|
||||
|
||||
override fun getOldListSize() = oldList.size
|
||||
|
||||
override fun getNewListSize() = newList.size
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition] == newList[newItemPosition]
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,625 +0,0 @@
|
|||
package com.lagradost.cloudstream3.ui.result
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromUrlNull
|
||||
import com.lagradost.cloudstream3.APIHolder.getId
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
|
||||
import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider
|
||||
import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider
|
||||
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu.getEpisodesDetails
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.player.IGenerator
|
||||
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.getFillerEpisodes
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.collections.set
|
||||
|
||||
const val EPISODE_RANGE_SIZE = 50
|
||||
const val EPISODE_RANGE_OVERLOAD = 60
|
||||
|
||||
class ResultViewModel : ViewModel() {
|
||||
private var repo: APIRepository? = null
|
||||
private var generator: IGenerator? = null
|
||||
|
||||
private val _resultResponse: MutableLiveData<Resource<LoadResponse>> = MutableLiveData()
|
||||
private val _episodes: MutableLiveData<List<ResultEpisode>> = MutableLiveData()
|
||||
private val episodeById: MutableLiveData<HashMap<Int, Int>> =
|
||||
MutableLiveData() // lookup by ID to get Index
|
||||
|
||||
private val _publicEpisodes: MutableLiveData<Resource<List<ResultEpisode>>> = MutableLiveData()
|
||||
private val _publicEpisodesCount: MutableLiveData<Int> = MutableLiveData() // before the sorting
|
||||
private val _rangeOptions: MutableLiveData<List<String>> = MutableLiveData()
|
||||
val selectedRange: MutableLiveData<String> = MutableLiveData()
|
||||
private val selectedRangeInt: MutableLiveData<Int> = MutableLiveData()
|
||||
val rangeOptions: LiveData<List<String>> = _rangeOptions
|
||||
|
||||
val result: LiveData<Resource<LoadResponse>> get() = _resultResponse
|
||||
|
||||
val episodes: LiveData<List<ResultEpisode>> get() = _episodes
|
||||
val publicEpisodes: LiveData<Resource<List<ResultEpisode>>> get() = _publicEpisodes
|
||||
val publicEpisodesCount: LiveData<Int> get() = _publicEpisodesCount
|
||||
|
||||
val dubStatus: LiveData<DubStatus> get() = _dubStatus
|
||||
private val _dubStatus: MutableLiveData<DubStatus> = MutableLiveData()
|
||||
|
||||
val id: MutableLiveData<Int> = MutableLiveData()
|
||||
val selectedSeason: MutableLiveData<Int> = MutableLiveData(-2)
|
||||
val seasonSelections: MutableLiveData<List<Int?>> = MutableLiveData()
|
||||
|
||||
val dubSubSelections: LiveData<Set<DubStatus>> get() = _dubSubSelections
|
||||
private val _dubSubSelections: MutableLiveData<Set<DubStatus>> = MutableLiveData()
|
||||
|
||||
val dubSubEpisodes: LiveData<Map<DubStatus, List<ResultEpisode>>?> get() = _dubSubEpisodes
|
||||
private val _dubSubEpisodes: MutableLiveData<Map<DubStatus, List<ResultEpisode>>?> =
|
||||
MutableLiveData()
|
||||
|
||||
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
|
||||
val watchStatus: LiveData<WatchType> get() = _watchStatus
|
||||
|
||||
fun updateWatchStatus(status: WatchType) = viewModelScope.launch {
|
||||
val currentId = id.value ?: return@launch
|
||||
_watchStatus.postValue(status)
|
||||
val resultPage = _resultResponse.value
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
setResultWatchState(currentId, status.internalId)
|
||||
if (resultPage != null && resultPage is Resource.Success) {
|
||||
val resultPageData = resultPage.value
|
||||
val current = getBookmarkedData(currentId)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
setBookmarkedData(
|
||||
currentId,
|
||||
DataStoreHelper.BookmarkedData(
|
||||
currentId,
|
||||
current?.bookmarkedTime ?: currentTime,
|
||||
currentTime,
|
||||
resultPageData.name,
|
||||
resultPageData.url,
|
||||
resultPageData.apiName,
|
||||
resultPageData.type,
|
||||
resultPageData.posterUrl,
|
||||
resultPageData.year
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "RVM"
|
||||
}
|
||||
|
||||
var lastMeta: SyncAPI.SyncResult? = null
|
||||
var lastSync: Map<String, String>? = null
|
||||
|
||||
private suspend fun applyMeta(
|
||||
resp: LoadResponse,
|
||||
meta: SyncAPI.SyncResult?,
|
||||
syncs: Map<String, String>? = null
|
||||
): Pair<LoadResponse, Boolean> {
|
||||
if (meta == null) return resp to false
|
||||
var updateEpisodes = false
|
||||
val out = resp.apply {
|
||||
Log.i(TAG, "applyMeta")
|
||||
|
||||
duration = duration ?: meta.duration
|
||||
rating = rating ?: meta.publicScore
|
||||
tags = tags ?: meta.genres
|
||||
plot = if (plot.isNullOrBlank()) meta.synopsis else plot
|
||||
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
|
||||
actors = actors ?: meta.actors
|
||||
|
||||
if (this is EpisodeResponse) {
|
||||
nextAiring = nextAiring ?: meta.nextAiring
|
||||
}
|
||||
|
||||
for ((k, v) in syncs ?: emptyMap()) {
|
||||
syncData[k] = v
|
||||
}
|
||||
|
||||
val realRecommendations = ArrayList<SearchResponse>()
|
||||
val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
|
||||
meta.recommendations?.forEach { rec ->
|
||||
apiNames.forEach { name ->
|
||||
realRecommendations.add(rec.copy(apiName = name))
|
||||
}
|
||||
}
|
||||
|
||||
recommendations = recommendations?.union(realRecommendations)?.toList()
|
||||
?: realRecommendations
|
||||
|
||||
argamap({
|
||||
addTrailer(meta.trailers)
|
||||
}, {
|
||||
if (this !is AnimeLoadResponse) return@argamap
|
||||
val map = getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false)
|
||||
if (map.isNullOrEmpty()) return@argamap
|
||||
updateEpisodes = DubStatus.values().map { dubStatus ->
|
||||
val current =
|
||||
this.episodes[dubStatus]?.mapIndexed { index, episode ->
|
||||
episode.apply {
|
||||
this.episode = this.episode ?: (index + 1)
|
||||
}
|
||||
}?.sortedBy { it.episode ?: 0 }?.toMutableList()
|
||||
if (current.isNullOrEmpty()) return@map false
|
||||
val episodeNumbers = current.map { ep -> ep.episode!! }
|
||||
var updateCount = 0
|
||||
map.forEach { (episode, node) ->
|
||||
episodeNumbers.binarySearch(episode).let { index ->
|
||||
current.getOrNull(index)?.let { currentEp ->
|
||||
current[index] = currentEp.apply {
|
||||
updateCount++
|
||||
val currentBack = this
|
||||
this.description = this.description ?: node.description?.en
|
||||
this.name = this.name ?: node.titles?.canonical
|
||||
this.episode = this.episode ?: node.num ?: episodeNumbers[index]
|
||||
this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.episodes[dubStatus] = current
|
||||
updateCount > 0
|
||||
}.any { it }
|
||||
})
|
||||
}
|
||||
return out to updateEpisodes
|
||||
}
|
||||
|
||||
fun setMeta(meta: SyncAPI.SyncResult, syncs: Map<String, String>?) =
|
||||
viewModelScope.launch {
|
||||
Log.i(TAG, "setMeta")
|
||||
lastMeta = meta
|
||||
lastSync = syncs
|
||||
val (value, updateEpisodes) = ioWork {
|
||||
(result.value as? Resource.Success<LoadResponse>?)?.value?.let { resp ->
|
||||
return@ioWork applyMeta(resp, meta, syncs)
|
||||
}
|
||||
return@ioWork null to null
|
||||
}
|
||||
_resultResponse.postValue(Resource.Success(value ?: return@launch))
|
||||
if (updateEpisodes ?: return@launch) updateEpisodes(value, lastShowFillers)
|
||||
}
|
||||
|
||||
private fun loadWatchStatus(localId: Int? = null) {
|
||||
val currentId = localId ?: id.value ?: return
|
||||
val currentWatch = getResultWatchState(currentId)
|
||||
_watchStatus.postValue(currentWatch)
|
||||
}
|
||||
|
||||
private fun filterEpisodes(list: List<ResultEpisode>?, selection: Int?, range: Int?) {
|
||||
if (list == null) return
|
||||
val seasonTypes = HashMap<Int?, Boolean>()
|
||||
for (i in list) {
|
||||
if (!seasonTypes.containsKey(i.season)) {
|
||||
seasonTypes[i.season] = true
|
||||
}
|
||||
}
|
||||
val seasons = seasonTypes.toList().map { it.first }.sortedBy { it }
|
||||
seasonSelections.postValue(seasons)
|
||||
if (seasons.isEmpty()) { // WHAT THE FUCK DID YOU DO????? HOW DID YOU DO THIS
|
||||
_publicEpisodes.postValue(Resource.Success(emptyList()))
|
||||
return
|
||||
}
|
||||
|
||||
val realSelection = if (!seasonTypes.containsKey(selection)) seasons.first() else selection
|
||||
val internalId = id.value
|
||||
|
||||
if (internalId != null) setResultSeason(internalId, realSelection)
|
||||
|
||||
selectedSeason.postValue(realSelection ?: -2)
|
||||
|
||||
var currentList = list.filter { it.season == realSelection }
|
||||
_publicEpisodesCount.postValue(currentList.size)
|
||||
|
||||
val rangeList = ArrayList<String>()
|
||||
for (i in currentList.indices step EPISODE_RANGE_SIZE) {
|
||||
if (i + EPISODE_RANGE_SIZE < currentList.size) {
|
||||
rangeList.add("${i + 1}-${i + EPISODE_RANGE_SIZE}")
|
||||
} else {
|
||||
rangeList.add("${i + 1}-${currentList.size}")
|
||||
}
|
||||
}
|
||||
|
||||
val cRange = range ?: if (selection != null) {
|
||||
0
|
||||
} else {
|
||||
selectedRangeInt.value ?: 0
|
||||
}
|
||||
|
||||
val realRange = if (cRange * EPISODE_RANGE_SIZE > currentList.size) {
|
||||
currentList.size / EPISODE_RANGE_SIZE
|
||||
} else {
|
||||
cRange
|
||||
}
|
||||
|
||||
if (currentList.size > EPISODE_RANGE_OVERLOAD) {
|
||||
currentList = currentList.subList(
|
||||
realRange * EPISODE_RANGE_SIZE,
|
||||
minOf(currentList.size, (realRange + 1) * EPISODE_RANGE_SIZE)
|
||||
)
|
||||
_rangeOptions.postValue(rangeList)
|
||||
selectedRangeInt.postValue(realRange)
|
||||
selectedRange.postValue(rangeList[realRange])
|
||||
} else {
|
||||
val allRange = "1-${currentList.size}"
|
||||
_rangeOptions.postValue(listOf(allRange))
|
||||
selectedRangeInt.postValue(0)
|
||||
selectedRange.postValue(allRange)
|
||||
}
|
||||
|
||||
_publicEpisodes.postValue(Resource.Success(currentList))
|
||||
}
|
||||
|
||||
fun changeSeason(selection: Int?) {
|
||||
filterEpisodes(_episodes.value, selection, null)
|
||||
}
|
||||
|
||||
fun changeRange(range: Int?) {
|
||||
filterEpisodes(_episodes.value, null, range)
|
||||
}
|
||||
|
||||
fun changeDubStatus(status: DubStatus?) {
|
||||
if (status == null) return
|
||||
dubSubEpisodes.value?.get(status)?.let { episodes ->
|
||||
id.value?.let {
|
||||
setDub(it, status)
|
||||
}
|
||||
_dubStatus.postValue(status!!)
|
||||
updateEpisodes(null, episodes, null)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadEpisode(
|
||||
episode: ResultEpisode,
|
||||
isCasting: Boolean,
|
||||
clearCache: Boolean = false
|
||||
): Resource<Pair<Set<ExtractorLink>, Set<SubtitleData>>> {
|
||||
return safeApiCall {
|
||||
val index = _episodes.value?.indexOf(episode) ?: episode.index
|
||||
|
||||
val currentLinks = mutableSetOf<ExtractorLink>()
|
||||
val currentSubs = mutableSetOf<SubtitleData>()
|
||||
|
||||
generator?.goto(index)
|
||||
generator?.generateLinks(clearCache, isCasting, {
|
||||
it.first?.let { link ->
|
||||
currentLinks.add(link)
|
||||
}
|
||||
}, { sub ->
|
||||
currentSubs.add(sub)
|
||||
})
|
||||
|
||||
return@safeApiCall Pair(
|
||||
currentLinks.toSet(),
|
||||
currentSubs.toSet()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getGenerator(episode: ResultEpisode): IGenerator? {
|
||||
val index = _episodes.value?.indexOf(episode) ?: episode.index
|
||||
|
||||
generator?.goto(index)
|
||||
return generator
|
||||
}
|
||||
|
||||
private fun updateEpisodes(localId: Int?, list: List<ResultEpisode>, selection: Int?) {
|
||||
_episodes.postValue(list)
|
||||
generator = RepoLinkGenerator(list)
|
||||
|
||||
val set = HashMap<Int, Int>()
|
||||
val range = selectedRangeInt.value
|
||||
|
||||
list.withIndex().forEach { set[it.value.id] = it.index }
|
||||
episodeById.postValue(set)
|
||||
|
||||
filterEpisodes(
|
||||
list,
|
||||
if (selection == -1) getResultSeason(localId ?: id.value ?: return) else selection,
|
||||
range
|
||||
)
|
||||
}
|
||||
|
||||
fun reloadEpisodes() {
|
||||
val current = _episodes.value ?: return
|
||||
val copy = current.map {
|
||||
val posDur = getViewPos(it.id)
|
||||
it.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0)
|
||||
}
|
||||
updateEpisodes(null, copy, selectedSeason.value)
|
||||
}
|
||||
|
||||
private fun filterName(name: String?): String? {
|
||||
if (name == null) return null
|
||||
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
|
||||
if (it.isEmpty())
|
||||
return null
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
var lastShowFillers = false
|
||||
private suspend fun updateEpisodes(loadResponse: LoadResponse, showFillers: Boolean) {
|
||||
Log.i(TAG, "updateEpisodes")
|
||||
try {
|
||||
lastShowFillers = showFillers
|
||||
val mainId = loadResponse.getId()
|
||||
|
||||
when (loadResponse) {
|
||||
is AnimeLoadResponse -> {
|
||||
if (loadResponse.episodes.isEmpty()) {
|
||||
_dubSubEpisodes.postValue(emptyMap())
|
||||
return
|
||||
}
|
||||
|
||||
// val status = getDub(mainId)
|
||||
val statuses = loadResponse.episodes.map { it.key }
|
||||
|
||||
// Extremely bruh to have to take in context here, but I'm not sure how to do this in a better way :(
|
||||
val preferDub = context?.getApiDubstatusSettings()
|
||||
?.contains(DubStatus.Dubbed) == true
|
||||
|
||||
// 3 statements because there can be only dub even if you do not prefer it.
|
||||
val dubStatus =
|
||||
if (preferDub && statuses.contains(DubStatus.Dubbed)) DubStatus.Dubbed
|
||||
else if (!preferDub && statuses.contains(DubStatus.Subbed)) DubStatus.Subbed
|
||||
else statuses.first()
|
||||
|
||||
val fillerEpisodes =
|
||||
if (showFillers) safeApiCall { getFillerEpisodes(loadResponse.name) } else null
|
||||
|
||||
val existingEpisodes = HashSet<Int>()
|
||||
val res = loadResponse.episodes.map { ep ->
|
||||
val episodes = ArrayList<ResultEpisode>()
|
||||
val idIndex = ep.key.id
|
||||
for ((index, i) in ep.value.withIndex()) {
|
||||
val episode = i.episode ?: (index + 1)
|
||||
val id = mainId + episode + idIndex * 1000000
|
||||
if (!existingEpisodes.contains(episode)) {
|
||||
existingEpisodes.add(id)
|
||||
episodes.add(buildResultEpisode(
|
||||
loadResponse.name,
|
||||
filterName(i.name),
|
||||
i.posterUrl,
|
||||
episode,
|
||||
i.season,
|
||||
i.data,
|
||||
loadResponse.apiName,
|
||||
id,
|
||||
index,
|
||||
i.rating,
|
||||
i.description,
|
||||
if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let {
|
||||
it.contains(episode) && it[episode] == true
|
||||
} ?: false else false,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Pair(ep.key, episodes)
|
||||
}.toMap()
|
||||
|
||||
// These posts needs to be in this order as to make the preferDub in ResultFragment work
|
||||
_dubSubEpisodes.postValue(res)
|
||||
res[dubStatus]?.let { episodes ->
|
||||
updateEpisodes(mainId, episodes, -1)
|
||||
}
|
||||
|
||||
_dubStatus.postValue(dubStatus)
|
||||
_dubSubSelections.postValue(loadResponse.episodes.keys)
|
||||
}
|
||||
|
||||
is TvSeriesLoadResponse -> {
|
||||
val episodes = ArrayList<ResultEpisode>()
|
||||
val existingEpisodes = HashSet<Int>()
|
||||
for ((index, episode) in loadResponse.episodes.sortedBy {
|
||||
(it.season?.times(10000) ?: 0) + (it.episode ?: 0)
|
||||
}.withIndex()) {
|
||||
val episodeIndex = episode.episode ?: (index + 1)
|
||||
val id =
|
||||
mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1
|
||||
if (!existingEpisodes.contains(id)) {
|
||||
existingEpisodes.add(id)
|
||||
episodes.add(
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
filterName(episode.name),
|
||||
episode.posterUrl,
|
||||
episodeIndex,
|
||||
episode.season,
|
||||
episode.data,
|
||||
loadResponse.apiName,
|
||||
id,
|
||||
index,
|
||||
episode.rating,
|
||||
episode.description,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
updateEpisodes(mainId, episodes, -1)
|
||||
}
|
||||
is MovieLoadResponse -> {
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
loadResponse.name,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
loadResponse.dataUrl,
|
||||
loadResponse.apiName,
|
||||
(mainId), // HAS SAME ID
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
).let {
|
||||
updateEpisodes(mainId, listOf(it), -1)
|
||||
}
|
||||
}
|
||||
is LiveStreamLoadResponse -> {
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
loadResponse.name,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
loadResponse.dataUrl,
|
||||
loadResponse.apiName,
|
||||
(mainId), // HAS SAME ID
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
).let {
|
||||
updateEpisodes(mainId, listOf(it), -1)
|
||||
}
|
||||
}
|
||||
is TorrentLoadResponse -> {
|
||||
updateEpisodes(
|
||||
mainId, listOf(
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
loadResponse.name,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
loadResponse.torrent ?: loadResponse.magnet ?: "",
|
||||
loadResponse.apiName,
|
||||
(mainId), // HAS SAME ID
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
)
|
||||
), -1
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
|
||||
_publicEpisodes.postValue(Resource.Loading())
|
||||
_resultResponse.postValue(Resource.Loading(url))
|
||||
|
||||
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
|
||||
if (api == null) {
|
||||
_resultResponse.postValue(
|
||||
Resource.Failure(
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
"This provider does not exist"
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val validUrlResource = safeApiCall {
|
||||
SyncRedirector.redirect(
|
||||
url,
|
||||
api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
|
||||
.replace(GogoanimeProvider().mainUrl, "gogoanime")
|
||||
)
|
||||
}
|
||||
|
||||
if (validUrlResource !is Resource.Success) {
|
||||
if (validUrlResource is Resource.Failure) {
|
||||
_resultResponse.postValue(validUrlResource)
|
||||
}
|
||||
|
||||
return@launch
|
||||
}
|
||||
val validUrl = validUrlResource.value
|
||||
|
||||
_resultResponse.postValue(Resource.Loading(validUrl))
|
||||
|
||||
_apiName.postValue(apiName)
|
||||
|
||||
repo = APIRepository(api)
|
||||
|
||||
val data = repo?.load(validUrl) ?: return@launch
|
||||
|
||||
_resultResponse.postValue(data)
|
||||
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
val loadResponse = if (lastMeta != null || lastSync != null) ioWork {
|
||||
applyMeta(data.value, lastMeta, lastSync).first
|
||||
} else data.value
|
||||
_resultResponse.postValue(Resource.Success(loadResponse))
|
||||
val mainId = loadResponse.getId()
|
||||
id.postValue(mainId)
|
||||
loadWatchStatus(mainId)
|
||||
|
||||
setKey(
|
||||
DOWNLOAD_HEADER_CACHE,
|
||||
mainId.toString(),
|
||||
VideoDownloadHelper.DownloadHeaderCached(
|
||||
apiName,
|
||||
validUrl,
|
||||
loadResponse.type,
|
||||
loadResponse.name,
|
||||
loadResponse.posterUrl,
|
||||
mainId,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
updateEpisodes(loadResponse, showFillers)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private var _apiName: MutableLiveData<String> = MutableLiveData()
|
||||
val apiName: LiveData<String> get() = _apiName
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,6 @@ import android.util.Log
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.apmap
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -12,8 +11,8 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApi
|
|||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.SyncUtil
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
|
||||
|
@ -44,9 +43,13 @@ class SyncViewModel : ViewModel() {
|
|||
|
||||
// prefix, id
|
||||
private var syncs = mutableMapOf<String, String>()
|
||||
private val _syncIds: MutableLiveData<MutableMap<String, String>> =
|
||||
MutableLiveData(mutableMapOf())
|
||||
val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds
|
||||
//private val _syncIds: MutableLiveData<MutableMap<String, String>> =
|
||||
// MutableLiveData(mutableMapOf())
|
||||
//val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds
|
||||
|
||||
fun getSyncs() : Map<String,String> {
|
||||
return syncs
|
||||
}
|
||||
|
||||
private val _currentSynced: MutableLiveData<List<CurrentSynced>> =
|
||||
MutableLiveData(getMissing())
|
||||
|
@ -76,7 +79,7 @@ class SyncViewModel : ViewModel() {
|
|||
Log.i(TAG, "addSync $idPrefix = $id")
|
||||
|
||||
syncs[idPrefix] = id
|
||||
_syncIds.postValue(syncs)
|
||||
//_syncIds.postValue(syncs)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -99,10 +102,10 @@ class SyncViewModel : ViewModel() {
|
|||
|
||||
var hasAddedFromUrl: HashSet<String> = hashSetOf()
|
||||
|
||||
fun addFromUrl(url: String?) = viewModelScope.launch {
|
||||
fun addFromUrl(url: String?) = ioSafe {
|
||||
Log.i(TAG, "addFromUrl = $url")
|
||||
|
||||
if (url == null || hasAddedFromUrl.contains(url)) return@launch
|
||||
if (url == null || hasAddedFromUrl.contains(url)) return@ioSafe
|
||||
SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) ->
|
||||
hasAddedFromUrl.add(url)
|
||||
|
||||
|
@ -166,7 +169,7 @@ class SyncViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun publishUserData() = viewModelScope.launch {
|
||||
fun publishUserData() = ioSafe {
|
||||
Log.i(TAG, "publishUserData")
|
||||
val user = userData.value
|
||||
if (user is Resource.Success) {
|
||||
|
@ -191,7 +194,7 @@ class SyncViewModel : ViewModel() {
|
|||
|
||||
/// modifies the current sync data, return null if you don't want to change it
|
||||
private fun modifyData(update: ((SyncAPI.SyncStatus) -> (SyncAPI.SyncStatus?))) =
|
||||
viewModelScope.launch {
|
||||
ioSafe {
|
||||
syncs.apmap { (prefix, id) ->
|
||||
repos.firstOrNull { it.idPrefix == prefix }?.let { repo ->
|
||||
if (repo.hasAccount()) {
|
||||
|
@ -209,7 +212,7 @@ class SyncViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateUserData() = viewModelScope.launch {
|
||||
fun updateUserData() = ioSafe {
|
||||
Log.i(TAG, "updateUserData")
|
||||
_userDataResponse.postValue(Resource.Loading())
|
||||
var lastError: Resource<SyncAPI.SyncStatus> = Resource.Failure(false, null, null, "No data")
|
||||
|
@ -219,7 +222,7 @@ class SyncViewModel : ViewModel() {
|
|||
val result = repo.getStatus(id)
|
||||
if (result is Resource.Success) {
|
||||
_userDataResponse.postValue(result)
|
||||
return@launch
|
||||
return@ioSafe
|
||||
} else if (result is Resource.Failure) {
|
||||
Log.e(TAG, "updateUserData error ${result.errorString}")
|
||||
lastError = result
|
||||
|
@ -230,7 +233,7 @@ class SyncViewModel : ViewModel() {
|
|||
_userDataResponse.postValue(lastError)
|
||||
}
|
||||
|
||||
private fun updateMetadata() = viewModelScope.launch {
|
||||
private fun updateMetadata() = ioSafe {
|
||||
Log.i(TAG, "updateMetadata")
|
||||
|
||||
_metaResponse.postValue(Resource.Loading())
|
||||
|
@ -253,7 +256,7 @@ class SyncViewModel : ViewModel() {
|
|||
val result = repo.getResult(id)
|
||||
if (result is Resource.Success) {
|
||||
_metaResponse.postValue(result)
|
||||
return@launch
|
||||
return@ioSafe
|
||||
} else if (result is Resource.Failure) {
|
||||
Log.e(
|
||||
TAG,
|
||||
|
|
163
app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
Normal file
163
app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
Normal file
|
@ -0,0 +1,163 @@
|
|||
package com.lagradost.cloudstream3.ui.result
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import com.lagradost.cloudstream3.mvvm.Some
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
|
||||
sealed class UiText {
|
||||
companion object {
|
||||
const val TAG = "UiText"
|
||||
}
|
||||
|
||||
data class DynamicString(val value: String) : UiText() {
|
||||
override fun toString(): String = value
|
||||
}
|
||||
class StringResource(
|
||||
@StringRes val resId: Int,
|
||||
val args: List<Any>
|
||||
) : UiText() {
|
||||
override fun toString(): String = "resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}"
|
||||
}
|
||||
|
||||
fun asStringNull(context: Context?): String? {
|
||||
try {
|
||||
return asString(context ?: return null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Got invalid data from $this")
|
||||
logError(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun asString(context: Context): String {
|
||||
return when (this) {
|
||||
is DynamicString -> value
|
||||
is StringResource -> {
|
||||
val str = context.getString(resId)
|
||||
if (args.isEmpty()) {
|
||||
str
|
||||
} else {
|
||||
str.format(*args.map {
|
||||
when (it) {
|
||||
is UiText -> it.asString(context)
|
||||
else -> it
|
||||
}
|
||||
}.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UiImage {
|
||||
data class Image(
|
||||
val url: String,
|
||||
val headers: Map<String, String>? = null,
|
||||
@DrawableRes val errorDrawable: Int? = null
|
||||
) : UiImage()
|
||||
|
||||
data class Drawable(@DrawableRes val resId: Int) : UiImage()
|
||||
}
|
||||
|
||||
fun ImageView?.setImage(value: UiImage?) {
|
||||
when (value) {
|
||||
is UiImage.Image -> setImageImage(value)
|
||||
is UiImage.Drawable -> setImageDrawable(value)
|
||||
null -> {
|
||||
this?.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ImageView?.setImageImage(value: UiImage.Image) {
|
||||
if (this == null) return
|
||||
this.isVisible = setImage(value.url, value.headers, value.errorDrawable)
|
||||
}
|
||||
|
||||
fun ImageView?.setImageDrawable(value: UiImage.Drawable) {
|
||||
if (this == null) return
|
||||
this.isVisible = true
|
||||
setImageResource(value.resId)
|
||||
}
|
||||
|
||||
@JvmName("imgNull")
|
||||
fun img(
|
||||
url: String?,
|
||||
headers: Map<String, String>? = null,
|
||||
@DrawableRes errorDrawable: Int? = null
|
||||
): UiImage? {
|
||||
if (url.isNullOrBlank()) return null
|
||||
return UiImage.Image(url, headers, errorDrawable)
|
||||
}
|
||||
|
||||
fun img(
|
||||
url: String,
|
||||
headers: Map<String, String>? = null,
|
||||
@DrawableRes errorDrawable: Int? = null
|
||||
): UiImage {
|
||||
return UiImage.Image(url, headers, errorDrawable)
|
||||
}
|
||||
|
||||
fun img(@DrawableRes drawable: Int): UiImage {
|
||||
return UiImage.Drawable(drawable)
|
||||
}
|
||||
|
||||
fun txt(value: String): UiText {
|
||||
return UiText.DynamicString(value)
|
||||
}
|
||||
|
||||
@JvmName("txtNull")
|
||||
fun txt(value: String?): UiText? {
|
||||
return UiText.DynamicString(value ?: return null)
|
||||
}
|
||||
|
||||
fun txt(@StringRes resId: Int, vararg args: Any): UiText {
|
||||
return UiText.StringResource(resId, args.toList())
|
||||
}
|
||||
|
||||
@JvmName("txtNull")
|
||||
fun txt(@StringRes resId: Int?, vararg args: Any?): UiText? {
|
||||
if (resId == null || args.any { it == null }) {
|
||||
return null
|
||||
}
|
||||
return UiText.StringResource(resId, args.filterNotNull().toList())
|
||||
}
|
||||
|
||||
fun TextView?.setText(text: UiText?) {
|
||||
if (this == null) return
|
||||
if (text == null) {
|
||||
this.isVisible = false
|
||||
} else {
|
||||
val str = text.asStringNull(context)
|
||||
this.isGone = str.isNullOrBlank()
|
||||
this.text = str
|
||||
}
|
||||
}
|
||||
|
||||
fun TextView?.setTextHtml(text: UiText?) {
|
||||
if (this == null) return
|
||||
if (text == null) {
|
||||
this.isVisible = false
|
||||
} else {
|
||||
val str = text.asStringNull(context)
|
||||
this.isGone = str.isNullOrBlank()
|
||||
this.text = str.html()
|
||||
}
|
||||
}
|
||||
|
||||
fun TextView?.setTextHtml(text: Some<UiText>?) {
|
||||
setTextHtml(if(text is Some.Success) text.value else null)
|
||||
}
|
||||
|
||||
fun TextView?.setText(text: Some<UiText>?) {
|
||||
setText(if(text is Some.Success) text.value else null)
|
||||
}
|
|
@ -27,7 +27,7 @@ object SearchHelper {
|
|||
} else {
|
||||
if (card.isFromDownload) {
|
||||
handleDownloadClick(
|
||||
activity, card.name, DownloadClickEvent(
|
||||
activity, DownloadClickEvent(
|
||||
DOWNLOAD_ACTION_PLAY_FILE,
|
||||
VideoDownloadHelper.DownloadEpisodeCached(
|
||||
card.name,
|
||||
|
|
|
@ -187,21 +187,21 @@ object AppUtils {
|
|||
@WorkerThread
|
||||
fun Context.addProgramsToContinueWatching(data: List<DataStoreHelper.ResumeWatchingResult>) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val context = this
|
||||
ioSafe {
|
||||
data.forEach { episodeInfo ->
|
||||
try {
|
||||
val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, this)
|
||||
val nextProgram = buildWatchNextProgramUri(this, episodeInfo)
|
||||
val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, context)
|
||||
val nextProgram = buildWatchNextProgramUri(context, episodeInfo)
|
||||
|
||||
// If the program is already in the Watch Next row, update it
|
||||
if (program != null && id != null) {
|
||||
PreviewChannelHelper(this).updateWatchNextProgram(
|
||||
PreviewChannelHelper(context).updateWatchNextProgram(
|
||||
nextProgram,
|
||||
id,
|
||||
)
|
||||
} else {
|
||||
PreviewChannelHelper(this)
|
||||
PreviewChannelHelper(context)
|
||||
.publishWatchNextProgram(nextProgram)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -12,7 +12,7 @@ object Coroutines {
|
|||
}
|
||||
}
|
||||
|
||||
fun ioSafe(work: suspend (() -> Unit)): Job {
|
||||
fun ioSafe(work: suspend (CoroutineScope.() -> Unit)): Job {
|
||||
return CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
work()
|
||||
|
@ -22,7 +22,7 @@ object Coroutines {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun <T> ioWork(work: suspend (() -> T)): T {
|
||||
suspend fun <T> ioWork(work: suspend (CoroutineScope.() -> T)): T {
|
||||
return withContext(Dispatchers.IO) {
|
||||
work()
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ const val RESULT_WATCH_STATE_DATA = "result_watch_state_data"
|
|||
const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes
|
||||
const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching"
|
||||
const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated"
|
||||
const val RESULT_EPISODE = "result_episode"
|
||||
const val RESULT_SEASON = "result_season"
|
||||
const val RESULT_DUB = "result_dub"
|
||||
|
||||
|
@ -163,7 +164,7 @@ object DataStoreHelper {
|
|||
)
|
||||
}
|
||||
|
||||
fun getLastWatchedOld(id: Int?): VideoDownloadHelper.ResumeWatching? {
|
||||
private fun getLastWatchedOld(id: Int?): VideoDownloadHelper.ResumeWatching? {
|
||||
if (id == null) return null
|
||||
return getKey(
|
||||
"$currentAccount/$RESULT_RESUME_WATCHING_OLD",
|
||||
|
@ -192,8 +193,9 @@ object DataStoreHelper {
|
|||
return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null)
|
||||
}
|
||||
|
||||
fun getDub(id: Int): DubStatus {
|
||||
return DubStatus.values()[getKey("$currentAccount/$RESULT_DUB", id.toString()) ?: 0]
|
||||
fun getDub(id: Int): DubStatus? {
|
||||
return DubStatus.values()
|
||||
.getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1)
|
||||
}
|
||||
|
||||
fun setDub(id: Int, status: DubStatus) {
|
||||
|
@ -221,14 +223,22 @@ object DataStoreHelper {
|
|||
)
|
||||
}
|
||||
|
||||
fun getResultSeason(id: Int): Int {
|
||||
return getKey("$currentAccount/$RESULT_SEASON", id.toString()) ?: -1
|
||||
fun getResultSeason(id: Int): Int? {
|
||||
return getKey("$currentAccount/$RESULT_SEASON", id.toString(), null)
|
||||
}
|
||||
|
||||
fun setResultSeason(id: Int, value: Int?) {
|
||||
setKey("$currentAccount/$RESULT_SEASON", id.toString(), value)
|
||||
}
|
||||
|
||||
fun getResultEpisode(id: Int): Int? {
|
||||
return getKey("$currentAccount/$RESULT_EPISODE", id.toString(), null)
|
||||
}
|
||||
|
||||
fun setResultEpisode(id: Int, value: Int?) {
|
||||
setKey("$currentAccount/$RESULT_EPISODE", id.toString(), value)
|
||||
}
|
||||
|
||||
fun addSync(id: Int, idPrefix: String, url: String) {
|
||||
setKey("${idPrefix}_sync", id.toString(), url)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,9 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.add_account_input.*
|
||||
import kotlinx.android.synthetic.main.add_account_input.text1
|
||||
import kotlinx.android.synthetic.main.bottom_selection_dialog_direct.*
|
||||
|
||||
object SingleSelectionHelper {
|
||||
fun Activity?.showOptionSelectStringRes(
|
||||
|
@ -21,7 +24,7 @@ object SingleSelectionHelper {
|
|||
tvOptions: List<Int> = listOf(),
|
||||
callback: (Pair<Boolean, Int>) -> Unit
|
||||
) {
|
||||
if(this == null) return
|
||||
if (this == null) return
|
||||
|
||||
this.showOptionSelect(
|
||||
view,
|
||||
|
@ -39,7 +42,7 @@ object SingleSelectionHelper {
|
|||
tvOptions: List<String>,
|
||||
callback: (Pair<Boolean, Int>) -> Unit
|
||||
) {
|
||||
if(this == null) return
|
||||
if (this == null) return
|
||||
|
||||
if (this.isTvSettings()) {
|
||||
val builder =
|
||||
|
@ -86,42 +89,44 @@ object SingleSelectionHelper {
|
|||
showApply: Boolean,
|
||||
isMultiSelect: Boolean,
|
||||
callback: (List<Int>) -> Unit,
|
||||
dismissCallback: () -> Unit
|
||||
dismissCallback: () -> Unit,
|
||||
itemLayout: Int = R.layout.sort_bottom_single_choice
|
||||
) {
|
||||
if(this == null) return
|
||||
if (this == null) return
|
||||
|
||||
val realShowApply = showApply || isMultiSelect
|
||||
val listView = dialog.findViewById<ListView>(R.id.listview1)!!
|
||||
val textView = dialog.findViewById<TextView>(R.id.text1)!!
|
||||
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
|
||||
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
|
||||
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
|
||||
val listView = dialog.listview1//.findViewById<ListView>(R.id.listview1)!!
|
||||
val textView = dialog.text1//.findViewById<TextView>(R.id.text1)!!
|
||||
val applyButton = dialog.apply_btt//.findViewById<TextView>(R.id.apply_btt)
|
||||
val cancelButton = dialog.cancel_btt//findViewById<TextView>(R.id.cancel_btt)
|
||||
val applyHolder = dialog.apply_btt_holder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
|
||||
|
||||
applyHolder.isVisible = realShowApply
|
||||
applyHolder?.isVisible = realShowApply
|
||||
if (!realShowApply) {
|
||||
val params = listView.layoutParams as LinearLayout.LayoutParams
|
||||
params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0)
|
||||
listView.layoutParams = params
|
||||
}
|
||||
|
||||
textView.text = name
|
||||
textView?.text = name
|
||||
textView?.isGone = name.isBlank()
|
||||
|
||||
val arrayAdapter = ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
|
||||
val arrayAdapter = ArrayAdapter<String>(this, itemLayout)
|
||||
arrayAdapter.addAll(items)
|
||||
|
||||
listView.adapter = arrayAdapter
|
||||
listView?.adapter = arrayAdapter
|
||||
if (isMultiSelect) {
|
||||
listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
||||
listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
||||
} else {
|
||||
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
}
|
||||
|
||||
for (select in selectedIndex) {
|
||||
listView.setItemChecked(select, true)
|
||||
listView?.setItemChecked(select, true)
|
||||
}
|
||||
|
||||
selectedIndex.minOrNull()?.let {
|
||||
listView.setSelection(it)
|
||||
listView?.setSelection(it)
|
||||
}
|
||||
|
||||
// var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1
|
||||
|
@ -130,7 +135,7 @@ object SingleSelectionHelper {
|
|||
dismissCallback.invoke()
|
||||
}
|
||||
|
||||
listView.setOnItemClickListener { _, _, which, _ ->
|
||||
listView?.setOnItemClickListener { _, _, which, _ ->
|
||||
// lastSelectedIndex = which
|
||||
if (realShowApply) {
|
||||
if (!isMultiSelect) {
|
||||
|
@ -142,7 +147,7 @@ object SingleSelectionHelper {
|
|||
}
|
||||
}
|
||||
if (realShowApply) {
|
||||
applyButton.setOnClickListener {
|
||||
applyButton?.setOnClickListener {
|
||||
val list = ArrayList<Int>()
|
||||
for (index in 0 until listView.count) {
|
||||
if (listView.checkedItemPositions[index])
|
||||
|
@ -151,7 +156,7 @@ object SingleSelectionHelper {
|
|||
callback.invoke(list)
|
||||
dialog.dismissSafe(this)
|
||||
}
|
||||
cancelButton.setOnClickListener {
|
||||
cancelButton?.setOnClickListener {
|
||||
dialog.dismissSafe(this)
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +171,7 @@ object SingleSelectionHelper {
|
|||
callback: (String) -> Unit,
|
||||
dismissCallback: () -> Unit
|
||||
) {
|
||||
if(this == null) return
|
||||
if (this == null) return
|
||||
|
||||
val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!!
|
||||
val textView = dialog.findViewById<TextView>(R.id.text1)!!
|
||||
|
@ -205,7 +210,7 @@ object SingleSelectionHelper {
|
|||
dismissCallback: () -> Unit,
|
||||
callback: (List<Int>) -> Unit,
|
||||
) {
|
||||
if(this == null) return
|
||||
if (this == null) return
|
||||
|
||||
val builder =
|
||||
AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
|
@ -224,7 +229,7 @@ object SingleSelectionHelper {
|
|||
dismissCallback: () -> Unit,
|
||||
callback: (Int) -> Unit,
|
||||
) {
|
||||
if(this == null) return
|
||||
if (this == null) return
|
||||
|
||||
val builder =
|
||||
AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
|
@ -271,6 +276,31 @@ object SingleSelectionHelper {
|
|||
)
|
||||
}
|
||||
|
||||
fun Activity.showBottomDialogInstant(
|
||||
items: List<String>,
|
||||
name: String,
|
||||
dismissCallback: () -> Unit,
|
||||
callback: (Int) -> Unit,
|
||||
): BottomSheetDialog {
|
||||
val builder =
|
||||
BottomSheetDialog(this)
|
||||
builder.setContentView(R.layout.bottom_selection_dialog_direct)
|
||||
|
||||
builder.show()
|
||||
showDialog(
|
||||
builder,
|
||||
items,
|
||||
listOf(),
|
||||
name,
|
||||
showApply = false,
|
||||
isMultiSelect = false,
|
||||
callback = { if (it.isNotEmpty()) callback.invoke(it.first()) },
|
||||
dismissCallback = dismissCallback,
|
||||
itemLayout = R.layout.sort_bottom_single_choice_no_checkmark
|
||||
)
|
||||
return builder
|
||||
}
|
||||
|
||||
fun Activity.showNginxTextInputDialog(
|
||||
name: String,
|
||||
value: String,
|
||||
|
|
33
app/src/main/res/layout/bottom_loading.xml
Normal file
33
app/src/main/res/layout/bottom_loading.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text1"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textStyle="bold"
|
||||
android:textSize="20sp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:text="@string/loading"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:layout_marginBottom="-6.5dp"
|
||||
android:indeterminate="true"
|
||||
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminateTint="?attr/colorPrimary"
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:progressTint="?attr/colorPrimary"
|
||||
android:layout_height="15dp">
|
||||
</androidx.core.widget.ContentLoadingProgressBar>
|
||||
</LinearLayout>
|
34
app/src/main/res/layout/bottom_selection_dialog_direct.xml
Normal file
34
app/src/main/res/layout/bottom_selection_dialog_direct.xml
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text1"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textStyle="bold"
|
||||
android:textSize="20sp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
tools:text="Test"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ListView
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
|
||||
android:id="@+id/listview1"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:paddingTop="10dp"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:listitem="@layout/sort_bottom_single_choice_no_checkmark"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_rowWeight="1" />
|
||||
</LinearLayout>
|
|
@ -12,7 +12,7 @@
|
|||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading_chromecast"
|
||||
android:text="@string/loading"
|
||||
android:layout_gravity="center"
|
||||
android:textColor="@color/textColor"
|
||||
android:textSize="20sp"
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
android:id="@+id/result_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
style="@style/DarkFragment"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
@ -290,15 +291,15 @@
|
|||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/result_poster_holder"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="140dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="@dimen/rounded_image_radius">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/result_poster"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="140dp"
|
||||
android:contentDescription="@string/result_poster_img_des"
|
||||
android:foreground="@drawable/outline_drawable"
|
||||
android:scaleType="centerCrop"
|
||||
|
@ -464,6 +465,14 @@
|
|||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="15sp"
|
||||
tools:text="@string/provider_info_meta" />
|
||||
<TextView
|
||||
android:id="@+id/result_no_episodes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="15sp"
|
||||
tools:text="@string/no_episodes_found" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_tag_holder"
|
||||
|
@ -669,7 +678,7 @@
|
|||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/result_series_parent"
|
||||
android:id="@+id/result_resume_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
|
@ -835,7 +844,6 @@
|
|||
<LinearLayout
|
||||
android:id="@+id/result_next_airing_holder"
|
||||
android:layout_gravity="start"
|
||||
android:paddingBottom="15dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
style="@style/AlertDialogCustom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/DarkFragment"
|
||||
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/quick_search_root"
|
||||
|
|
|
@ -12,6 +12,6 @@
|
|||
android:scaleType="fitCenter"
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/default_cover"
|
||||
android:background="#fffff0"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:contentDescription="@string/poster_image" />
|
||||
</LinearLayout>
|
|
@ -0,0 +1,22 @@
|
|||
<!--<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"
|
||||
android:layout_height="?android:attr/listPreferredItemHeightSmall"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="?attr/textColor"
|
||||
tools:text="Example Text"
|
||||
android:background="?attr/bitDarkerGrayBackground"
|
||||
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/NoCheckLabel"
|
||||
tools:text="hello"
|
||||
android:textStyle="normal"
|
||||
android:textColor="?attr/textColor"
|
||||
android:id="@android:id/text1" />
|
|
@ -29,7 +29,7 @@
|
|||
<string name="result_share">شارك</string>
|
||||
<string name="result_open_in_browser">فتح في الويب </string>
|
||||
<string name="skip_loading">تخطي التحميل</string>
|
||||
<string name="loading_chromecast">…تحميل</string>
|
||||
<string name="loading">…تحميل</string>
|
||||
|
||||
<string name="type_watching">مشاهدة</string>
|
||||
<string name="type_on_hold">في الانتظار</string>
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
<string name="result_share">Compartilhar</string>
|
||||
<string name="result_open_in_browser">Abrir no Navegador</string>
|
||||
<string name="skip_loading">Pular Carregamento</string>
|
||||
<string name="loading_chromecast">Carregando…</string>
|
||||
<string name="loading">Carregando…</string>
|
||||
|
||||
<string name="type_watching">Assistindo</string>
|
||||
<string name="type_on_hold">Em espera</string>
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
<string name="result_share">Sdílet</string>
|
||||
<string name="result_open_in_browser">Otevřít v prohlížeči</string>
|
||||
<string name="skip_loading">Přeskočit načítání</string>
|
||||
<string name="loading_chromecast">Načítání…</string>
|
||||
<string name="loading">Načítání…</string>
|
||||
|
||||
<string name="type_watching">Sledování</string>
|
||||
<string name="type_on_hold">Pozastaveno</string>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<string name="result_share">Teilen</string>
|
||||
<string name="result_open_in_browser">Im Browser öffnen</string>
|
||||
<string name="skip_loading">Buffern überspringen</string>
|
||||
<string name="loading_chromecast">Lädt…</string>
|
||||
<string name="loading">Lädt…</string>
|
||||
<string name="type_watching">Am schauen</string>
|
||||
<string name="type_on_hold">Pausiert</string>
|
||||
<string name="type_completed">Abgeschlossen</string>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<string name="result_share">Μοίρασε</string>
|
||||
<string name="result_open_in_browser">Άνοιγμα στον περιηγητή</string>
|
||||
<string name="skip_loading">Προσπέραση φορτώματος</string>
|
||||
<string name="loading_chromecast">Φόρτωση…</string>
|
||||
<string name="loading">Φόρτωση…</string>
|
||||
|
||||
<string name="type_watching">Watching</string>
|
||||
<string name="type_on_hold">On-Hold</string>
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<string name="result_share">Compartir</string>
|
||||
<string name="result_open_in_browser">Abrir en el navegador</string>
|
||||
<string name="skip_loading">Omitir carga</string>
|
||||
<string name="loading_chromecast">Cargando…</string>
|
||||
<string name="loading">Cargando…</string>
|
||||
|
||||
<string name="type_watching">Viendo</string>
|
||||
<string name="type_on_hold">En espera</string>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<string name="result_share">Partager</string>
|
||||
<string name="result_open_in_browser">Ouvrir dans le naviguateur</string>
|
||||
<string name="skip_loading">Passer le chargement</string>
|
||||
<string name="loading_chromecast">Chargement…</string>
|
||||
<string name="loading">Chargement…</string>
|
||||
<string name="type_watching">En visionnage</string>
|
||||
<string name="type_on_hold">En pose</string>
|
||||
<string name="type_completed">Terminé</string>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
<string name="result_share">Bagikan</string>
|
||||
<string name="result_open_in_browser">Buka Di Browser</string>
|
||||
<string name="skip_loading">Skip Loading</string>
|
||||
<string name="loading_chromecast">Loading…</string>
|
||||
<string name="loading">Loading…</string>
|
||||
|
||||
<string name="type_watching">Sedang Menonton</string>
|
||||
<string name="type_on_hold">Tertahan</string>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
<string name="result_share">Condividi</string>
|
||||
<string name="result_open_in_browser">Apri nel browser</string>
|
||||
<string name="skip_loading">Salta caricamento</string>
|
||||
<string name="loading_chromecast">Caricamento…</string>
|
||||
<string name="loading">Caricamento…</string>
|
||||
|
||||
<string name="type_watching">Guardando</string>
|
||||
<string name="type_on_hold">In attesa</string>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<string name="result_share">Сподели</string>
|
||||
<string name="result_open_in_browser">Отвори во прелистувач</string>
|
||||
<string name="skip_loading">Прескокни вчитување</string>
|
||||
<string name="loading_chromecast">Вчитување…</string>
|
||||
<string name="loading">Вчитување…</string>
|
||||
|
||||
<string name="type_watching">Моментални гледања</string>
|
||||
<string name="type_on_hold">Ставено на чекање</string>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<string name="result_share">aauuh</string>
|
||||
<string name="result_open_in_browser">oooohh oooohhhaaaoouuh</string>
|
||||
<string name="skip_loading">oooohhooooo</string>
|
||||
<string name="loading_chromecast">ooh aaahhu</string>
|
||||
<string name="loading">ooh aaahhu</string>
|
||||
<string name="type_watching">aaaghh ooo-ahah</string>
|
||||
<string name="type_on_hold">aaahhu</string>
|
||||
<string name="type_completed">ahhahooo</string>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<string name="result_share">Deel</string>
|
||||
<string name="result_open_in_browser">Openen in Browser</string>
|
||||
<string name="skip_loading">Laden overslaan</string>
|
||||
<string name="loading_chromecast">Laden…</string>
|
||||
<string name="loading">Laden…</string>
|
||||
|
||||
<string name="type_watching">Aan het kijken</string>
|
||||
<string name="type_on_hold">In de wacht</string>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<string name="result_share">Dele</string>
|
||||
<string name="result_open_in_browser">Åpne i nettleseren</string>
|
||||
<string name="skip_loading">Hopp over</string>
|
||||
<string name="loading_chromecast">Laster inn…</string>
|
||||
<string name="loading">Laster inn…</string>
|
||||
|
||||
<string name="type_watching">Ser på</string>
|
||||
<string name="type_on_hold">På vent</string>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<string name="result_share">Udostępnij</string>
|
||||
<string name="result_open_in_browser">Otwórz w przeglądarce</string>
|
||||
<string name="skip_loading">Pomiń ładowanie</string>
|
||||
<string name="loading_chromecast">Ładowanie…</string>
|
||||
<string name="loading">Ładowanie…</string>
|
||||
|
||||
<string name="type_watching">W trakcie</string>
|
||||
<string name="type_on_hold">Zawieszone</string>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<string name="result_share">Compartir</string>
|
||||
<string name="result_open_in_browser">Abrir no Navegador</string>
|
||||
<string name="skip_loading">Saltar Carga</string>
|
||||
<string name="loading_chromecast">Cargando…</string>
|
||||
<string name="loading">Cargando…</string>
|
||||
|
||||
<string name="type_watching">Assistindo</string>
|
||||
<string name="type_on_hold">Em espera</string>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
<string name="result_share">Distribuie</string>
|
||||
<string name="result_open_in_browser">Deschide în browser</string>
|
||||
<string name="skip_loading">Săriți încărcarea</string>
|
||||
<string name="loading_chromecast">Se încarcă...</string>
|
||||
<string name="loading">Se încarcă...</string>
|
||||
|
||||
<string name="type_watching">În curs de vizualizare</string>
|
||||
<string name="type_on_hold">În așteptare</string>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<string name="result_share">Dela</string>
|
||||
<string name="result_open_in_browser">Öppna i webbläsaren</string>
|
||||
<string name="skip_loading">Hoppa över</string>
|
||||
<string name="loading_chromecast">Laddar…</string>
|
||||
<string name="loading">Laddar…</string>
|
||||
|
||||
<string name="type_watching">Tittar på</string>
|
||||
<string name="type_on_hold">Pausad</string>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<string name="result_share">I-share</string>
|
||||
<string name="result_open_in_browser">Buksan sa browser</string>
|
||||
<string name="skip_loading">Skip Loading…</string>
|
||||
<string name="loading_chromecast">Loading…</string>
|
||||
<string name="loading">Loading…</string>
|
||||
|
||||
<string name="type_watching">Pinapanood</string>
|
||||
<string name="type_on_hold">Inihinto</string>
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<string name="result_share">Paylaş</string>
|
||||
<string name="result_open_in_browser">Tarayıcıda aç</string>
|
||||
<string name="skip_loading">Yüklemeyi atla</string>
|
||||
<string name="loading_chromecast">Yükleniyor…</string>
|
||||
<string name="loading">Yükleniyor…</string>
|
||||
|
||||
<string name="type_watching">İzleniyor</string>
|
||||
<string name="type_on_hold">Beklemede</string>
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
<string name="result_share">Chia sẻ</string>
|
||||
<string name="result_open_in_browser">Mở bằng trình duyệt</string>
|
||||
<string name="skip_loading">Bỏ qua</string>
|
||||
<string name="loading_chromecast">Đang tải…</string>
|
||||
<string name="loading">Đang tải…</string>
|
||||
|
||||
<string name="type_watching">Đang xem</string>
|
||||
<string name="type_on_hold">Đang chờ</string>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<string name="result_share">分享</string>
|
||||
<string name="result_open_in_browser">在浏览器中打开</string>
|
||||
<string name="skip_loading">跳过加载</string>
|
||||
<string name="loading_chromecast">正在加载…</string>
|
||||
<string name="loading">正在加载…</string>
|
||||
|
||||
<string name="type_watching">正在观看</string>
|
||||
<string name="type_on_hold">暂时搁置</string>
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
<string name="result_share">Share</string>
|
||||
<string name="result_open_in_browser">Open In Browser</string>
|
||||
<string name="skip_loading">Skip Loading</string>
|
||||
<string name="loading_chromecast">Loading…</string>
|
||||
<string name="loading">Loading…</string>
|
||||
|
||||
<string name="type_watching">Watching</string>
|
||||
<string name="type_on_hold">On-Hold</string>
|
||||
|
@ -286,9 +286,12 @@
|
|||
</string>
|
||||
|
||||
<string name="season">Season</string>
|
||||
<string name="season_format">%s %d</string>
|
||||
<string name="no_season">No Season</string>
|
||||
<string name="episode">Episode</string>
|
||||
<string name="episodes">Episodes</string>
|
||||
<string name="episodes_range">%d-%d</string>
|
||||
<string name="episode_format" formatted="true">%d %s</string>
|
||||
<string name="season_short">S</string>
|
||||
<string name="episode_short">E</string>
|
||||
<string name="no_episodes_found">No Episodes found</string>
|
||||
|
|
|
@ -266,6 +266,7 @@
|
|||
</style>
|
||||
|
||||
<style name="AppBottomSheetDialogTheme">
|
||||
<item name="android:navigationBarColor">?attr/boxItemBackground</item>
|
||||
<item name="android:windowCloseOnTouchOutside">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowAnimationStyle">@style/Animation.Design.BottomSheetDialog</item>
|
||||
|
@ -278,7 +279,7 @@
|
|||
<item name="behavior_skipCollapsed">true</item>
|
||||
<item name="shapeAppearance">@null</item>
|
||||
<item name="shapeAppearanceOverlay">@null</item>
|
||||
<item name="backgroundTint">?android:attr/colorBackground</item>
|
||||
<item name="backgroundTint">@color/transparent</item>
|
||||
<item name="android:background">@drawable/rounded_dialog</item>
|
||||
<item name="behavior_peekHeight">512dp</item>
|
||||
</style>
|
||||
|
@ -334,6 +335,9 @@
|
|||
<item name="tabMode">scrollable</item>-->
|
||||
</style>
|
||||
|
||||
<style name="DarkFragment" parent="AppTheme">
|
||||
<item name="android:navigationBarColor">?attr/colorPrimary</item>
|
||||
</style>
|
||||
<style name="AlertDialogCustom" parent="Theme.AppCompat.Dialog.Alert">
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:textColor">?attr/textColor</item>
|
||||
|
@ -448,7 +452,15 @@
|
|||
<item name="android:textColor">?attr/textColor</item>
|
||||
</style>
|
||||
|
||||
<style name="CheckLabel" parent="@style/AppTextViewStyle">
|
||||
<style name="CheckLabel" parent="@style/NoCheckLabel">
|
||||
|
||||
<!-- <item name="drawableTint">@color/check_selection_color</item>-->
|
||||
<!-- Set color in the drawable instead of tint to allow multiple drawables-->
|
||||
<item name="android:checkMark">?android:attr/listChoiceIndicatorSingle</item>
|
||||
<item name="drawableStartCompat">@drawable/ic_baseline_check_24_listview</item>
|
||||
</style>
|
||||
|
||||
<style name="NoCheckLabel" parent="@style/AppTextViewStyle">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:minHeight">?android:attr/listPreferredItemHeightSmall</item>
|
||||
|
@ -458,15 +470,13 @@
|
|||
<item name="android:gravity">center_vertical</item>
|
||||
<item name="android:paddingStart">12dp</item>
|
||||
<item name="android:paddingEnd">12dp</item>
|
||||
<item name="android:checkMark">?android:attr/listChoiceIndicatorSingle</item>
|
||||
<item name="android:ellipsize">marquee</item>
|
||||
<item name="android:foreground">?attr/selectableItemBackgroundBorderless</item>
|
||||
<item name="android:drawablePadding">20dp</item>
|
||||
<!-- <item name="drawableTint">@color/check_selection_color</item>-->
|
||||
<!-- Set color in the drawable instead of tint to allow multiple drawables-->
|
||||
<item name="drawableStartCompat">@drawable/ic_baseline_check_24_listview</item>
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
<style name="BlackButton" parent="NiceButton">
|
||||
<item name="strokeColor">?attr/textColor</item>
|
||||
<item name="backgroundTint">?attr/iconGrayBackground</item>
|
||||
|
|
|
@ -543,7 +543,7 @@
|
|||
"TrailersTwoProvider": {
|
||||
"language": "en",
|
||||
"name": "Trailers.to",
|
||||
"status": 1,
|
||||
"status": 0,
|
||||
"url": "https://trailers.to"
|
||||
},
|
||||
"TwoEmbedProvider": {
|
||||
|
|
292
providers.json
292
providers.json
|
@ -1,292 +0,0 @@
|
|||
{
|
||||
"AkwamProvider": {
|
||||
"name": "Akwam",
|
||||
"url": "https://akwam.to",
|
||||
"status": 1
|
||||
},
|
||||
"AllAnimeProvider": {
|
||||
"name": "AllAnime",
|
||||
"url": "https://allanime.site",
|
||||
"status": 1
|
||||
},
|
||||
"AllMoviesForYouProvider": {
|
||||
"name": "AllMoviesForYou",
|
||||
"url": "https://allmoviesforyou.net",
|
||||
"status": 1
|
||||
},
|
||||
"AnimeFlickProvider": {
|
||||
"name": "AnimeFlick",
|
||||
"url": "https://animeflick.net",
|
||||
"status": 1
|
||||
},
|
||||
"AnimePaheProvider": {
|
||||
"name": "AnimePahe",
|
||||
"url": "https://animepahe.com",
|
||||
"status": 0
|
||||
},
|
||||
"AnimeWorldProvider": {
|
||||
"name": "AnimeWorld",
|
||||
"url": "https://www.animeworld.tv",
|
||||
"status": 1
|
||||
},
|
||||
"AnimeflvnetProvider": {
|
||||
"name": "Animeflv.net",
|
||||
"url": "https://www3.animeflv.net",
|
||||
"status": 1
|
||||
},
|
||||
"AnimekisaProvider": {
|
||||
"name": "Animekisa",
|
||||
"url": "https://animekisa.in",
|
||||
"status": 1
|
||||
},
|
||||
"AsianLoadProvider": {
|
||||
"name": "AsianLoad",
|
||||
"url": "https://asianembed.io",
|
||||
"status": 1
|
||||
},
|
||||
"AsiaFlixProvider": {
|
||||
"name": "AsiaFlix",
|
||||
"url": "https://asiaflix.app",
|
||||
"status": 0
|
||||
},
|
||||
"BflixProvider": {
|
||||
"name": "Bflix",
|
||||
"url": "https://bflix.ru",
|
||||
"status": 0
|
||||
},
|
||||
"FmoviesToProvider": {
|
||||
"name": "Fmovies.to",
|
||||
"url": "https://fmovies.to",
|
||||
"status": 0
|
||||
},
|
||||
"SflixProProvider": {
|
||||
"name": "Sflix.pro",
|
||||
"url": "https://sflix.pro",
|
||||
"status": 0
|
||||
},
|
||||
"CinecalidadProvider": {
|
||||
"name": "Cinecalidad",
|
||||
"url": "https://cinecalidad.lol",
|
||||
"status": 1
|
||||
},
|
||||
"CrossTmdbProvider": {
|
||||
"name": "MultiMovie",
|
||||
"url": "NONE",
|
||||
"status": 1
|
||||
},
|
||||
"CuevanaProvider": {
|
||||
"name": "Cuevana",
|
||||
"url": "https://cuevana3.me",
|
||||
"status": 1
|
||||
},
|
||||
"DoramasYTProvider": {
|
||||
"name": "DoramasYT",
|
||||
"url": "https://doramasyt.com",
|
||||
"status": 1
|
||||
},
|
||||
"DramaSeeProvider": {
|
||||
"name": "DramaSee",
|
||||
"url": "https://dramasee.net",
|
||||
"status": 1
|
||||
},
|
||||
"DubbedAnimeProvider": {
|
||||
"name": "DubbedAnime",
|
||||
"url": "https://bestdubbedanime.com",
|
||||
"status": 1
|
||||
},
|
||||
"EgyBestProvider": {
|
||||
"name": "EgyBest",
|
||||
"url": "https://egy.best",
|
||||
"status": 0
|
||||
},
|
||||
"EntrepeliculasyseriesProvider": {
|
||||
"name": "EntrePeliculasySeries",
|
||||
"url": "https://entrepeliculasyseries.nu",
|
||||
"status": 1
|
||||
},
|
||||
"FilmanProvider": {
|
||||
"name": "filman.cc",
|
||||
"url": "https://filman.cc",
|
||||
"status": 1
|
||||
},
|
||||
"FrenchStreamProvider": {
|
||||
"name": "French Stream",
|
||||
"url": "https://french-stream.re",
|
||||
"status": 1
|
||||
},
|
||||
"GogoanimeProvider": {
|
||||
"name": "GogoAnime",
|
||||
"url": "https://gogoanime.sk",
|
||||
"status": 1
|
||||
},
|
||||
"KawaiifuProvider": {
|
||||
"name": "Kawaiifu",
|
||||
"url": "https://kawaiifu.com",
|
||||
"status": 1
|
||||
},
|
||||
"HDMProvider": {
|
||||
"name": "HD Movies",
|
||||
"url": "https://hdm.to",
|
||||
"status": 0
|
||||
},
|
||||
"IHaveNoTvProvider": {
|
||||
"name": "I Have No TV",
|
||||
"url": "https://ihavenotv.com",
|
||||
"status": 1
|
||||
},
|
||||
"KdramaHoodProvider": {
|
||||
"name": "KDramaHood",
|
||||
"url": "https://kdramahood.com",
|
||||
"status": 1
|
||||
},
|
||||
"LookMovieProvider": {
|
||||
"name": "LookMovie",
|
||||
"url": "https://lookmovie.io",
|
||||
"status": 0
|
||||
},
|
||||
"MeloMovieProvider": {
|
||||
"name": "MeloMovie",
|
||||
"url": "https://melomovie.com",
|
||||
"status": 0
|
||||
},
|
||||
"MonoschinosProvider": {
|
||||
"name": "Monoschinos",
|
||||
"url": "https://monoschinos2.com",
|
||||
"status": 1
|
||||
},
|
||||
"MyCimaProvider": {
|
||||
"name": "MyCima",
|
||||
"url": "https://mycima.tv",
|
||||
"status": 1
|
||||
},
|
||||
"NineAnimeProvider": {
|
||||
"name": "9Anime",
|
||||
"url": "https://9anime.id",
|
||||
"status": 0
|
||||
},
|
||||
"PeliSmartProvider": {
|
||||
"name": "PeliSmart",
|
||||
"url": "https://pelismart.com",
|
||||
"status": 1
|
||||
},
|
||||
"PelisflixProvider": {
|
||||
"name": "Pelisflix",
|
||||
"url": "https://pelisflix.li",
|
||||
"status": 1
|
||||
},
|
||||
"PelisplusHDProvider": {
|
||||
"name": "PelisplusHD",
|
||||
"url": "https://pelisplushd.net",
|
||||
"status": 1
|
||||
},
|
||||
"PelisplusProvider": {
|
||||
"name": "Pelisplus",
|
||||
"url": "https://pelisplus.icu",
|
||||
"status": 1
|
||||
},
|
||||
"PinoyHDXyzProvider": {
|
||||
"name": "Pinoy-HD",
|
||||
"url": "https://www.pinoy-hd.xyz",
|
||||
"status": 1
|
||||
},
|
||||
"PinoyMoviePediaProvider": {
|
||||
"name": "Pinoy Moviepedia",
|
||||
"url": "https://pinoymoviepedia.ru",
|
||||
"status": 1
|
||||
},
|
||||
"PinoyMoviesEsProvider": {
|
||||
"name": "Pinoy Movies",
|
||||
"url": "https://pinoymovies.es",
|
||||
"status": 1
|
||||
},
|
||||
"SflixProvider": {
|
||||
"name": "Sflix.to",
|
||||
"url": "https://sflix.to",
|
||||
"status": 1
|
||||
},
|
||||
"DopeboxProvider": {
|
||||
"name": "Dopebox",
|
||||
"url": "https://dopebox.to",
|
||||
"status": 1
|
||||
},
|
||||
"SolarmovieProvider": {
|
||||
"name": "Solarmovie",
|
||||
"url": "https://solarmovie.pe",
|
||||
"status": 1
|
||||
},
|
||||
"SeriesflixProvider": {
|
||||
"name": "Seriesflix",
|
||||
"url": "https://seriesflix.video",
|
||||
"status": 1
|
||||
},
|
||||
"SoaptwoDayProvider": {
|
||||
"name": "Soap2Day",
|
||||
"url": "https://secretlink.xyz",
|
||||
"status": 0
|
||||
},
|
||||
"TenshiProvider": {
|
||||
"name": "Tenshi.moe",
|
||||
"url": "https://tenshi.moe",
|
||||
"status": 1
|
||||
},
|
||||
"TrailersTwoProvider": {
|
||||
"name": "Trailers.to",
|
||||
"url": "https://trailers.to",
|
||||
"status": 1
|
||||
},
|
||||
"TheFlixToProvider": {
|
||||
"name": "TheFlix.to",
|
||||
"url": "https://theflix.to",
|
||||
"status": 0
|
||||
},
|
||||
"TwoEmbedProvider": {
|
||||
"name": "2Embed",
|
||||
"url": "https://www.2embed.to",
|
||||
"status": 1
|
||||
},
|
||||
"VMoveeProvider": {
|
||||
"name": "VMovee",
|
||||
"url": "https://www.vmovee.watch",
|
||||
"status": 1
|
||||
},
|
||||
"VfFilmProvider": {
|
||||
"name": "vf-film.me",
|
||||
"url": "https://vf-film.me",
|
||||
"status": 1
|
||||
},
|
||||
"VfSerieProvider": {
|
||||
"name": "vf-serie.org",
|
||||
"url": "https://vf-serie.org",
|
||||
"status": 1
|
||||
},
|
||||
"VidEmbedProvider": {
|
||||
"name": "VidEmbed",
|
||||
"url": "https://vidembed.cc",
|
||||
"status": 1
|
||||
},
|
||||
"YomoviesProvider": {
|
||||
"name": "Yomovies",
|
||||
"url": "https://yomovies.vip",
|
||||
"status": 1
|
||||
},
|
||||
"WatchAsianProvider": {
|
||||
"name": "WatchAsian",
|
||||
"url": "https://watchasian.cx",
|
||||
"status": 1
|
||||
},
|
||||
"WatchCartoonOnlineProvider": {
|
||||
"name": "WatchCartoonOnline",
|
||||
"url": "https://www.wcostream.com",
|
||||
"status": 1
|
||||
},
|
||||
"WcoProvider": {
|
||||
"name": "WCO Stream",
|
||||
"url": "https://wcostream.cc",
|
||||
"status": 1
|
||||
},
|
||||
"ZoroProvider": {
|
||||
"name": "Zoro",
|
||||
"url": "https://zoro.to",
|
||||
"status": 0
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue