forked from recloudstream/cloudstream
		
	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> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue