Merge pull request #2 from reduplicated/newResultViewModel

New result view model
This commit is contained in:
reduplicated 2022-08-04 16:13:27 +02:00 committed by GitHub
commit cd9bdb8ba7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 2750 additions and 2400 deletions

View file

@ -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 {

View file

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

View file

@ -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")

View file

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

View file

@ -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 {

View file

@ -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) {

View file

@ -84,7 +84,7 @@ class DownloadChildFragment : Fragment() {
DownloadChildAdapter(
ArrayList(),
) { click ->
handleDownloadClick(activity, name, click)
handleDownloadClick(activity, click)
}
downloadDeleteEventListener = { id: Int ->

View file

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

View file

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

View file

@ -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]
}

View file

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

View file

@ -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,

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

View file

@ -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,

View file

@ -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) {

View file

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

View file

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

View file

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

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

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

View file

@ -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"

View file

@ -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">

View file

@ -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">

View file

@ -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"

View file

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

View file

@ -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" />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -543,7 +543,7 @@
"TrailersTwoProvider": {
"language": "en",
"name": "Trailers.to",
"status": 1,
"status": 0,
"url": "https://trailers.to"
},
"TwoEmbedProvider": {

View file

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