forked from recloudstream/cloudstream
		
	sync stuff
This commit is contained in:
		
							parent
							
								
									e41542bef4
								
							
						
					
					
						commit
						2a27c0360d
					
				
					 16 changed files with 729 additions and 542 deletions
				
			
		|  | @ -266,7 +266,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|             if (str.contains(appString)) { | ||||
|                 for (api in OAuth2Apis) { | ||||
|                     if (str.contains("/${api.redirectUrl}")) { | ||||
|                         try { | ||||
|                             api.handleRedirect(str) | ||||
|                         } catch (e : Exception) { | ||||
|                             logError(e) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|  |  | |||
|  | @ -43,8 +43,8 @@ interface OAuth2API { | |||
| 
 | ||||
|         // used for active syncing | ||||
|         val SyncApis | ||||
|             get() = listOf<SyncAPI>( | ||||
|                 malApi, aniListApi | ||||
|             get() = listOf( | ||||
|                 SyncRepo(malApi), SyncRepo(aniListApi) | ||||
|             ) | ||||
| 
 | ||||
|         const val appString = "cloudstreamapp" | ||||
|  |  | |||
|  | @ -3,6 +3,26 @@ package com.lagradost.cloudstream3.syncproviders | |||
| import com.lagradost.cloudstream3.ShowStatus | ||||
| 
 | ||||
| interface SyncAPI : OAuth2API { | ||||
|     val icon: Int | ||||
|     val mainUrl: String | ||||
| 
 | ||||
|     /** | ||||
|     -1 -> None | ||||
|     0 -> Watching | ||||
|     1 -> Completed | ||||
|     2 -> OnHold | ||||
|     3 -> Dropped | ||||
|     4 -> PlanToWatch | ||||
|     5 -> ReWatching | ||||
|      */ | ||||
|     suspend fun score(id: String, status: SyncStatus): Boolean | ||||
| 
 | ||||
|     suspend fun getStatus(id: String): SyncStatus? | ||||
| 
 | ||||
|     suspend fun getResult(id: String): SyncResult? | ||||
| 
 | ||||
|     suspend fun search(name: String): List<SyncSearchResult>? | ||||
| 
 | ||||
|     data class SyncSearchResult( | ||||
|         val name: String, | ||||
|         val syncApiName: String, | ||||
|  | @ -48,7 +68,7 @@ interface SyncAPI : OAuth2API { | |||
|         var synopsis: String? = null, | ||||
|         var airStatus: ShowStatus? = null, | ||||
|         var nextAiring: SyncNextAiring? = null, | ||||
|         var studio: String? = null, | ||||
|         var studio: List<String>? = null, | ||||
|         var genres: List<String>? = null, | ||||
|         var trailerUrl: String? = null, | ||||
| 
 | ||||
|  | @ -62,24 +82,4 @@ interface SyncAPI : OAuth2API { | |||
|         var actors: List<SyncActor>? = null, | ||||
|         var characters: List<SyncCharacter>? = null, | ||||
|     ) | ||||
| 
 | ||||
|     val icon: Int | ||||
| 
 | ||||
|     val mainUrl: String | ||||
|     suspend fun search(name: String): List<SyncSearchResult>? | ||||
| 
 | ||||
|     /** | ||||
|     -1 -> None | ||||
|     0 -> Watching | ||||
|     1 -> Completed | ||||
|     2 -> OnHold | ||||
|     3 -> Dropped | ||||
|     4 -> PlanToWatch | ||||
|     5 -> ReWatching | ||||
|      */ | ||||
|     suspend fun score(id: String, status: SyncStatus): Boolean | ||||
| 
 | ||||
|     suspend fun getStatus(id: String): SyncStatus? | ||||
| 
 | ||||
|     suspend fun getResult(id: String): SyncResult? | ||||
| } | ||||
|  | @ -0,0 +1,25 @@ | |||
| package com.lagradost.cloudstream3.syncproviders | ||||
| 
 | ||||
| import com.lagradost.cloudstream3.ErrorLoadingException | ||||
| import com.lagradost.cloudstream3.mvvm.Resource | ||||
| import com.lagradost.cloudstream3.mvvm.safeApiCall | ||||
| 
 | ||||
| class SyncRepo(private val repo: SyncAPI) { | ||||
|     val idPrefix get() = repo.idPrefix | ||||
| 
 | ||||
|     suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> { | ||||
|         return safeApiCall { repo.score(id, status) } | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getStatus(id : String) : Resource<SyncAPI.SyncStatus>  { | ||||
|         return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getResult(id : String) : Resource<SyncAPI.SyncResult>  { | ||||
|         return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") } | ||||
|     } | ||||
| 
 | ||||
|     suspend fun search(query : String) : Resource<List<SyncAPI.SyncSearchResult>> { | ||||
|         return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() } | ||||
|     } | ||||
| } | ||||
|  | @ -55,7 +55,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|     } | ||||
| 
 | ||||
|     override fun handleRedirect(url: String) { | ||||
|         try { | ||||
|         val sanitizer = | ||||
|             splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR | ||||
|         val token = sanitizer["access_token"]!! | ||||
|  | @ -70,9 +69,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         ioSafe { | ||||
|             getUser() | ||||
|         } | ||||
|         } catch (e: Exception) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? { | ||||
|  | @ -338,7 +334,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|     } | ||||
| 
 | ||||
|     fun initGetUser() { | ||||
|         if (getKey<String>(accountId, ANILIST_TOKEN_KEY, null) == null) return | ||||
|         if (getAuth() == null) return | ||||
|         ioSafe { | ||||
|             getUser() | ||||
|         } | ||||
|  | @ -351,7 +347,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         )!! | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getDataAboutId(id: Int): AniListTitleHolder? { | ||||
|     private suspend fun getDataAboutId(id: Int): AniListTitleHolder? { | ||||
|         val q = | ||||
|             """query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id) | ||||
|                 Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query) | ||||
|  | @ -369,18 +365,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                     } | ||||
|                 } | ||||
|             }""" | ||||
|         try { | ||||
|             val data = postApi("https://graphql.anilist.co", q, true) | ||||
|             var d: GetDataRoot? = null | ||||
|             try { | ||||
|                 d = mapper.readValue<GetDataRoot>(data) | ||||
|             } catch (e: Exception) { | ||||
|                 logError(e) | ||||
|                 println("AniList json failed") | ||||
|             } | ||||
|             if (d == null) { | ||||
|                 return null | ||||
|             } | ||||
| 
 | ||||
|         val data = postApi(q, true) | ||||
|         val d = mapper.readValue<GetDataRoot>(data ?: return null) | ||||
| 
 | ||||
|         val main = d.data.Media | ||||
|         if (main.mediaListEntry != null) { | ||||
|  | @ -404,24 +391,22 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                 type = AniListStatusType.None, | ||||
|             ) | ||||
|         } | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             return null | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun postApi(url: String, q: String, cache: Boolean = false): String { | ||||
|         return try { | ||||
|             if (!checkToken()) { | ||||
|                 // println("VARS_ " + vars) | ||||
|     private fun getAuth(): String? { | ||||
|         return getKey( | ||||
|             accountId, | ||||
|             ANILIST_TOKEN_KEY | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun postApi(q: String, cache: Boolean = false): String? { | ||||
|         return if (!checkToken()) { | ||||
|             app.post( | ||||
|                 "https://graphql.anilist.co/", | ||||
|                 headers = mapOf( | ||||
|                         "Authorization" to "Bearer " + getKey( | ||||
|                             accountId, | ||||
|                             ANILIST_TOKEN_KEY, | ||||
|                             "" | ||||
|                         )!!, | ||||
|                     "Authorization" to "Bearer " + (getAuth() ?: return null), | ||||
|                     if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" | ||||
|                 ), | ||||
|                 cacheTime = 0, | ||||
|  | @ -429,11 +414,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                 timeout = 5 // REASONABLE TIMEOUT | ||||
|             ).text.replace("\\/", "/") | ||||
|         } else { | ||||
|                 "" | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             "" | ||||
|             null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -515,12 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|     } | ||||
| 
 | ||||
|     suspend fun getAnilistAnimeListSmart(): Array<Lists>? { | ||||
|         if (getKey<String>( | ||||
|                 accountId, | ||||
|                 ANILIST_TOKEN_KEY, | ||||
|                 null | ||||
|             ) == null | ||||
|         ) return null | ||||
|         if (getAuth() == null) return null | ||||
| 
 | ||||
|         if (checkToken()) return null | ||||
|         return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) { | ||||
|  | @ -536,7 +512,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|     } | ||||
| 
 | ||||
|     private suspend fun getFullAnilistList(): FullAnilistList? { | ||||
|         try { | ||||
|         var userID: Int? = null | ||||
|         /** WARNING ASSUMES ONE USER! **/ | ||||
|         getKeys(ANILIST_USER_KEY)?.forEach { key -> | ||||
|  | @ -588,13 +563,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                     } | ||||
|                     } | ||||
|             """ | ||||
|             val text = postApi("https://graphql.anilist.co", query) | ||||
|             return text.toKotlinObject() | ||||
| 
 | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             return null | ||||
|         } | ||||
|         val text = postApi(query) | ||||
|         return text?.toKotlinObject() | ||||
|     } | ||||
| 
 | ||||
|     suspend fun toggleLike(id: Int): Boolean { | ||||
|  | @ -610,7 +580,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
| 					} | ||||
| 				} | ||||
| 			}""" | ||||
|         val data = postApi("https://graphql.anilist.co", q) | ||||
|         val data = postApi(q) | ||||
|         return data != "" | ||||
|     } | ||||
| 
 | ||||
|  | @ -620,7 +590,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         score: Int?, | ||||
|         progress: Int? | ||||
|     ): Boolean { | ||||
|         try { | ||||
|         val q = | ||||
|             """mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ | ||||
|                 aniListStatusString[maxOf( | ||||
|  | @ -635,12 +604,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                     score | ||||
|                 } | ||||
|                 }""" | ||||
|             val data = postApi("https://graphql.anilist.co", q) | ||||
|         val data = postApi(q) | ||||
|         return data != "" | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun getUser(setSettings: Boolean = true): AniListUser? { | ||||
|  | @ -661,10 +626,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                         } | ||||
|   					} | ||||
| 				}""" | ||||
|         try { | ||||
|             val data = postApi("https://graphql.anilist.co", q) | ||||
|         val data = postApi(q) | ||||
|         if (data == "") return null | ||||
|             val userData = mapper.readValue<AniListRoot>(data) | ||||
|         val userData = mapper.readValue<AniListRoot>(data ?: return null) | ||||
|         val u = userData.data.Viewer | ||||
|         val user = AniListUser( | ||||
|             u.id, | ||||
|  | @ -680,10 +644,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|             println("FFAV:" + i.id) | ||||
|         }*/ | ||||
|         return user | ||||
|         } catch (e: java.lang.Exception) { | ||||
|             logError(e) | ||||
|             return null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getAllSeasons(id: Int): List<SeasonResponse?> { | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | |||
| import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||
| import com.lagradost.cloudstream3.R | ||||
| import com.lagradost.cloudstream3.ShowStatus | ||||
| import com.lagradost.cloudstream3.app | ||||
| import com.lagradost.cloudstream3.mvvm.logError | ||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager | ||||
|  | @ -45,20 +46,28 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|     override fun loginInfo(): OAuth2API.LoginInfo? { | ||||
|         //getMalUser(true)? | ||||
|         getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user -> | ||||
|             return OAuth2API.LoginInfo(profilePicture = user.picture, name = user.name, accountIndex = accountIndex) | ||||
|             return OAuth2API.LoginInfo( | ||||
|                 profilePicture = user.picture, | ||||
|                 name = user.name, | ||||
|                 accountIndex = accountIndex | ||||
|             ) | ||||
|         } | ||||
|         return null | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> { | ||||
|         val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" | ||||
|         val auth = getKey<String>( | ||||
|     private fun getAuth(): String? { | ||||
|         return getKey( | ||||
|             accountId, | ||||
|             MAL_TOKEN_KEY | ||||
|         ) ?: return emptyList() | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> { | ||||
|         val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" | ||||
|         val auth = getAuth() ?: return emptyList() | ||||
|         val res = app.get( | ||||
|             url, headers = mapOf( | ||||
|                 "Authorization" to "Bearer " + auth, | ||||
|                 "Authorization" to "Bearer $auth", | ||||
|             ), cacheTime = 0 | ||||
|         ).text | ||||
|         return mapper.readValue<MalSearch>(res).data.map { | ||||
|  | @ -82,25 +91,156 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     data class MalAnime( | ||||
|         @JsonProperty("id") val id: Int?, | ||||
|         @JsonProperty("title") val title: String?, | ||||
|         @JsonProperty("main_picture") val mainPicture: MainPicture?, | ||||
|         @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, | ||||
|         @JsonProperty("start_date") val startDate: String?, | ||||
|         @JsonProperty("end_date") val endDate: String?, | ||||
|         @JsonProperty("synopsis") val synopsis: String?, | ||||
|         @JsonProperty("mean") val mean: Double?, | ||||
|         @JsonProperty("rank") val rank: Int?, | ||||
|         @JsonProperty("popularity") val popularity: Int?, | ||||
|         @JsonProperty("num_list_users") val numListUsers: Int?, | ||||
|         @JsonProperty("num_scoring_users") val numScoringUsers: Int?, | ||||
|         @JsonProperty("nsfw") val nsfw: String?, | ||||
|         @JsonProperty("created_at") val createdAt: String?, | ||||
|         @JsonProperty("updated_at") val updatedAt: String?, | ||||
|         @JsonProperty("media_type") val mediaType: String?, | ||||
|         @JsonProperty("status") val status: String?, | ||||
|         @JsonProperty("genres") val genres: ArrayList<Genres>, | ||||
|         @JsonProperty("my_list_status") val myListStatus: MyListStatus?, | ||||
|         @JsonProperty("num_episodes") val numEpisodes: Int?, | ||||
|         @JsonProperty("start_season") val startSeason: StartSeason?, | ||||
|         @JsonProperty("broadcast") val broadcast: Broadcast?, | ||||
|         @JsonProperty("source") val source: String?, | ||||
|         @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, | ||||
|         @JsonProperty("rating") val rating: String?, | ||||
|         @JsonProperty("pictures") val pictures: ArrayList<MainPicture>, | ||||
|         @JsonProperty("background") val background: String?, | ||||
|         @JsonProperty("related_anime") val relatedAnime: ArrayList<RelatedAnime>, | ||||
|         @JsonProperty("related_manga") val relatedManga: ArrayList<String>, | ||||
|         @JsonProperty("recommendations") val recommendations: ArrayList<Recommendations>, | ||||
|         @JsonProperty("studios") val studios: ArrayList<Studios>, | ||||
|         @JsonProperty("statistics") val statistics: Statistics?, | ||||
|     ) | ||||
| 
 | ||||
|     data class Recommendations( | ||||
|         @JsonProperty("node") val node: Node? = null, | ||||
|         @JsonProperty("num_recommendations") val numRecommendations: Int? = null | ||||
|     ) | ||||
| 
 | ||||
|     data class Studios( | ||||
|         @JsonProperty("id") val id: Int? = null, | ||||
|         @JsonProperty("name") val name: String? = null | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
|     data class MyListStatus( | ||||
|         @JsonProperty("status") val status: String? = null, | ||||
|         @JsonProperty("score") val score: Int? = null, | ||||
|         @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int? = null, | ||||
|         @JsonProperty("is_rewatching") val isRewatching: Boolean? = null, | ||||
|         @JsonProperty("updated_at") val updatedAt: String? = null | ||||
|     ) | ||||
| 
 | ||||
|     data class RelatedAnime( | ||||
|         @JsonProperty("node") val node: Node? = null, | ||||
|         @JsonProperty("relation_type") val relationType: String? = null, | ||||
|         @JsonProperty("relation_type_formatted") val relationTypeFormatted: String? = null | ||||
|     ) | ||||
| 
 | ||||
|     data class Status( | ||||
|         @JsonProperty("watching") val watching: String? = null, | ||||
|         @JsonProperty("completed") val completed: String? = null, | ||||
|         @JsonProperty("on_hold") val onHold: String? = null, | ||||
|         @JsonProperty("dropped") val dropped: String? = null, | ||||
|         @JsonProperty("plan_to_watch") val planToWatch: String? = null | ||||
|     ) | ||||
| 
 | ||||
|     data class Statistics( | ||||
|         @JsonProperty("status") val status: Status? = null, | ||||
|         @JsonProperty("num_list_users") val numListUsers: Int? = null | ||||
|     ) | ||||
| 
 | ||||
|     private fun parseDate(string: String?): Long? { | ||||
|         return try { | ||||
|             SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time | ||||
|         } catch (e: Exception) { | ||||
|             null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun toSearchResult(node : Node?) : SyncAPI.SyncSearchResult? { | ||||
|         return SyncAPI.SyncSearchResult( | ||||
|             name = node?.title ?: return null, | ||||
|             syncApiName = this.name, | ||||
|             id = node.id.toString(), | ||||
|             url = "https://myanimelist.net/anime/${node.id}", | ||||
|             posterUrl = node.main_picture?.large | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getResult(id: String): SyncAPI.SyncResult? { | ||||
|         val internalId = id.toIntOrNull() ?: return null | ||||
|         TODO("Not yet implemented") | ||||
|         val url = | ||||
|             "https://api.myanimelist.net/v2/anime/$internalId?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,created_at,updated_at,media_type,status,genres,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,related_manga,recommendations,studios,statistics" | ||||
|         val res = app.get( | ||||
|             url, headers = mapOf( | ||||
|                 "Authorization" to "Bearer " + (getAuth() ?: return null) | ||||
|             ) | ||||
|         ).text | ||||
|         return mapper.readValue<MalAnime>(res).let { malAnime -> | ||||
|             SyncAPI.SyncResult( | ||||
|                 id = malAnime.id?.toString()!!, | ||||
|                 totalEpisodes = malAnime.numEpisodes, | ||||
|                 title = malAnime.title, | ||||
|                 publicScore = malAnime.mean?.toFloat()?.times(100)?.toInt(), | ||||
|                 duration = malAnime.averageEpisodeDuration, | ||||
|                 synopsis = malAnime.synopsis, | ||||
|                 airStatus = when (malAnime.status) { | ||||
|                     "finished_airing" -> ShowStatus.Completed | ||||
|                     "airing" -> ShowStatus.Ongoing | ||||
|                     else -> null | ||||
|                 }, | ||||
|                 nextAiring = null, | ||||
|                 studio = malAnime.studios.mapNotNull { it.name }, | ||||
|                 genres = malAnime.genres.map { it.name }, | ||||
|                 trailerUrl = null, | ||||
|                 startDate = parseDate(malAnime.startDate), | ||||
|                 endDate = parseDate(malAnime.endDate), | ||||
|                 recommendations = malAnime.recommendations.mapNotNull { rec -> | ||||
|                     val node = rec.node ?: return@mapNotNull null | ||||
|                     toSearchResult(node) | ||||
|                 }, | ||||
|                 nextSeason = malAnime.relatedAnime.firstOrNull { | ||||
|                     return@firstOrNull it.relationType == "sequel" | ||||
|                 }?.let { toSearchResult(it.node)  }, | ||||
|                 prevSeason = malAnime.relatedAnime.firstOrNull { | ||||
|                     return@firstOrNull it.relationType == "prequel" | ||||
|                 }?.let { toSearchResult(it.node) }, | ||||
|                 actors = null, | ||||
|                 characters = null, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { | ||||
|         val internalId = id.toIntOrNull() ?: return null | ||||
| 
 | ||||
|         val data = getDataAboutMalId(internalId)?.my_list_status ?: return null | ||||
|         val data = getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status") | ||||
|         return SyncAPI.SyncStatus( | ||||
|             score = data.score, | ||||
|             status = malStatusAsString.indexOf(data.status), | ||||
|             score = data?.score, | ||||
|             status = malStatusAsString.indexOf(data?.status), | ||||
|             isFavorite = null, | ||||
|             watchedEpisodes = data.num_episodes_watched, | ||||
|             watchedEpisodes = data?.num_episodes_watched, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private val malStatusAsString = arrayOf("watching", "completed", "on_hold", "dropped", "plan_to_watch") | ||||
|         private val malStatusAsString = | ||||
|             arrayOf("watching", "completed", "on_hold", "dropped", "plan_to_watch") | ||||
| 
 | ||||
|         const val MAL_USER_KEY: String = "mal_user" // user data like profile | ||||
|         const val MAL_CACHED_LIST: String = "mal_cached_list" | ||||
|  | @ -111,7 +251,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|     } | ||||
| 
 | ||||
|     override fun handleRedirect(url: String) { | ||||
|         try { | ||||
|         val sanitizer = | ||||
|             splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR | ||||
|         val state = sanitizer["state"]!! | ||||
|  | @ -142,9 +281,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                 } | ||||
|             } | ||||
|         } | ||||
|         } catch (e: Exception) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun authenticate() { | ||||
|  | @ -277,30 +413,23 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         @JsonProperty("start_time") val start_time: String? | ||||
|     ) | ||||
| 
 | ||||
|     fun getMalAnimeListCached(): Array<Data>? { | ||||
|     private fun getMalAnimeListCached(): Array<Data>? { | ||||
|         return getKey(MAL_CACHED_LIST) as? Array<Data> | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getMalAnimeListSmart(): Array<Data>? { | ||||
|         if (getKey<String>( | ||||
|                 accountId, | ||||
|                 MAL_TOKEN_KEY | ||||
|             ) == null | ||||
|         ) return null | ||||
|         if (getAuth() == null) return null | ||||
|         return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) { | ||||
|             val list = getMalAnimeList() | ||||
|             if (list != null) { | ||||
|             setKey(MAL_CACHED_LIST, list) | ||||
|             setKey(MAL_SHOULD_UPDATE_LIST, false) | ||||
|             } | ||||
|             list | ||||
|         } else { | ||||
|             getMalAnimeListCached() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun getMalAnimeList(): Array<Data>? { | ||||
|         return try { | ||||
|     private suspend fun getMalAnimeList(): Array<Data> { | ||||
|         checkMalToken() | ||||
|         var offset = 0 | ||||
|         val fullList = mutableListOf<Data>() | ||||
|  | @ -308,13 +437,11 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         while (true) { | ||||
|             val data: MalList = getMalAnimeListSlice(offset) ?: break | ||||
|             fullList.addAll(data.data) | ||||
|                 offset = data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } ?: break | ||||
|             } | ||||
|             fullList.toTypedArray() | ||||
|             //mapper.readValue<MalAnime>(res) | ||||
|         } catch (e: Exception) { | ||||
|             null | ||||
|             offset = | ||||
|                 data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } | ||||
|                     ?: break | ||||
|         } | ||||
|         return fullList.toTypedArray() | ||||
|     } | ||||
| 
 | ||||
|     fun convertToStatus(string: String): MalStatusType { | ||||
|  | @ -323,11 +450,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
| 
 | ||||
|     private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? { | ||||
|         val user = "@me" | ||||
|         val auth = getKey<String>( | ||||
|             accountId, | ||||
|             MAL_TOKEN_KEY | ||||
|         ) ?: return null | ||||
|         return try { | ||||
|         val auth = getAuth() ?: return null | ||||
|         // Very lackluster docs | ||||
|         // https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get | ||||
|         val url = | ||||
|  | @ -337,29 +460,20 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                 "Authorization" to "Bearer $auth", | ||||
|             ), cacheTime = 0 | ||||
|         ).text | ||||
|             res.toKotlinObject() | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             null | ||||
|         } | ||||
|         return res.toKotlinObject() | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun getDataAboutMalId(id: Int): MalAnime? { | ||||
|         return try { | ||||
|     private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? { | ||||
|         // https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get | ||||
|             val url = "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status" | ||||
|         val url = | ||||
|             "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status" | ||||
|         val res = app.get( | ||||
|             url, headers = mapOf( | ||||
|                     "Authorization" to "Bearer " + getKey<String>( | ||||
|                         accountId, | ||||
|                         MAL_TOKEN_KEY | ||||
|                     )!! | ||||
|                 "Authorization" to "Bearer " + (getAuth() ?: return null) | ||||
|             ), cacheTime = 0 | ||||
|         ).text | ||||
|             mapper.readValue<MalAnime>(res) | ||||
|         } catch (e: Exception) { | ||||
|             null | ||||
|         } | ||||
| 
 | ||||
|         return mapper.readValue<SmallMalAnime>(res) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun setAllMalData() { | ||||
|  | @ -372,14 +486,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|             val res = app.get( | ||||
|                 "https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}", | ||||
|                 headers = mapOf( | ||||
|                     "Authorization" to "Bearer " + getKey<String>( | ||||
|                         accountId, | ||||
|                         MAL_TOKEN_KEY | ||||
|                     )!! | ||||
|                     "Authorization" to "Bearer " + (getAuth() ?: return) | ||||
|                 ), cacheTime = 0 | ||||
|             ).text | ||||
|             val values = mapper.readValue<MalRoot>(res) | ||||
|             val titles = values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) } | ||||
|             val titles = | ||||
|                 values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) } | ||||
|             for (t in titles) { | ||||
|                 allTitles[t.id] = t | ||||
|             } | ||||
|  | @ -389,7 +501,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|     } | ||||
| 
 | ||||
|     fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { | ||||
|         try { | ||||
|         // No time remaining if the show has already ended | ||||
|         try { | ||||
|             endDate?.let { | ||||
|  | @ -412,7 +523,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
| 
 | ||||
|         val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") | ||||
|         dateFormat.timeZone = TimeZone.getTimeZone("Japan") | ||||
|             val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null | ||||
|         val parsedDate = | ||||
|             dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null | ||||
|         val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000 | ||||
| 
 | ||||
|         // if it has already aired this week add a week to the timer | ||||
|  | @ -420,10 +532,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|             if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff | ||||
|         return secondsToReadable(updatedTimeDiff.toInt(), "Now") | ||||
| 
 | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|         } | ||||
|         return null | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun checkMalToken() { | ||||
|  | @ -438,14 +546,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
| 
 | ||||
|     private suspend fun getMalUser(setSettings: Boolean = true): MalUser? { | ||||
|         checkMalToken() | ||||
|         return try { | ||||
|         val res = app.get( | ||||
|             "https://api.myanimelist.net/v2/users/@me", | ||||
|             headers = mapOf( | ||||
|                     "Authorization" to "Bearer " + getKey<String>( | ||||
|                         accountId, | ||||
|                         MAL_TOKEN_KEY | ||||
|                     )!! | ||||
|                 "Authorization" to "Bearer " + (getAuth() ?: return null) | ||||
|             ), cacheTime = 0 | ||||
|         ).text | ||||
| 
 | ||||
|  | @ -454,11 +558,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|             setKey(accountId, MAL_USER_KEY, user) | ||||
|             registerAccount() | ||||
|         } | ||||
|             user | ||||
|         } catch (e: Exception) { | ||||
|             e.printStackTrace() | ||||
|             null | ||||
|         } | ||||
|         return user | ||||
|     } | ||||
| 
 | ||||
|     enum class MalStatusType(var value: Int) { | ||||
|  | @ -483,7 +583,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     suspend fun setScoreRequest( | ||||
|     private suspend fun setScoreRequest( | ||||
|         id: Int, | ||||
|         status: MalStatusType? = null, | ||||
|         score: Int? = null, | ||||
|  | @ -495,8 +595,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|             score, | ||||
|             num_watched_episodes | ||||
|         ) | ||||
|         if (res != "") { | ||||
|             return try { | ||||
| 
 | ||||
|         return if (res.isNullOrBlank()) { | ||||
|             false | ||||
|         } else { | ||||
|             val malStatus = mapper.readValue<MalStatus>(res) | ||||
|             if (allTitles.containsKey(id)) { | ||||
|                 val currentTitle = allTitles[id]!! | ||||
|  | @ -505,12 +607,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                 allTitles[id] = MalTitleHolder(malStatus, id, "") | ||||
|             } | ||||
|             true | ||||
|             } catch (e: Exception) { | ||||
|                 logError(e) | ||||
|                 false | ||||
|             } | ||||
|         } else { | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -519,15 +615,11 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         status: String? = null, | ||||
|         score: Int? = null, | ||||
|         num_watched_episodes: Int? = null, | ||||
|     ): String { | ||||
|         return try { | ||||
|             app.put( | ||||
|     ): String? { | ||||
|         return app.put( | ||||
|             "https://api.myanimelist.net/v2/anime/$id/my_list_status", | ||||
|             headers = mapOf( | ||||
|                     "Authorization" to "Bearer " + getKey<String>( | ||||
|                         accountId, | ||||
|                         MAL_TOKEN_KEY | ||||
|                     )!! | ||||
|                 "Authorization" to "Bearer " + (getAuth() ?: return null) | ||||
|             ), | ||||
|             data = mapOf( | ||||
|                 "status" to status, | ||||
|  | @ -535,10 +627,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                 "num_watched_episodes" to num_watched_episodes?.toString() | ||||
|             ) | ||||
|         ).text | ||||
|         } catch (e: Exception) { | ||||
|             e.printStackTrace() | ||||
|             return "" | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -591,7 +679,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|     ) | ||||
| 
 | ||||
|     // Used for getDataAboutId() | ||||
|     data class MalAnime( | ||||
|     data class SmallMalAnime( | ||||
|         @JsonProperty("id") val id: Int, | ||||
|         @JsonProperty("title") val title: String?, | ||||
|         @JsonProperty("num_episodes") val num_episodes: Int, | ||||
|  |  | |||
|  | @ -12,8 +12,7 @@ import androidx.fragment.app.Fragment | |||
| import androidx.fragment.app.activityViewModels | ||||
| import androidx.recyclerview.widget.GridLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.lagradost.cloudstream3.APIHolder.apis | ||||
| import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings | ||||
| import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia | ||||
| import com.lagradost.cloudstream3.HomePageList | ||||
| import com.lagradost.cloudstream3.R | ||||
| import com.lagradost.cloudstream3.mvvm.Resource | ||||
|  | @ -21,8 +20,11 @@ import com.lagradost.cloudstream3.mvvm.logError | |||
| import com.lagradost.cloudstream3.mvvm.observe | ||||
| import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList | ||||
| import com.lagradost.cloudstream3.ui.home.ParentItemAdapter | ||||
| import com.lagradost.cloudstream3.ui.search.* | ||||
| import com.lagradost.cloudstream3.ui.search.SearchAdapter | ||||
| import com.lagradost.cloudstream3.ui.search.SearchClickCallback | ||||
| import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse | ||||
| import com.lagradost.cloudstream3.ui.search.SearchHelper | ||||
| import com.lagradost.cloudstream3.ui.search.SearchViewModel | ||||
| import com.lagradost.cloudstream3.utils.UIHelper | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.navigate | ||||
|  | @ -31,11 +33,10 @@ import kotlinx.android.synthetic.main.fragment_search.* | |||
| import kotlinx.android.synthetic.main.quick_search.* | ||||
| import java.util.concurrent.locks.ReentrantLock | ||||
| 
 | ||||
| class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | ||||
| class QuickSearchFragment : Fragment() { | ||||
|     companion object { | ||||
|         fun pushSearch(activity: Activity?, autoSearch: String? = null) { | ||||
|             activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { | ||||
|                 putBoolean("mainapi", true) | ||||
|                 autoSearch?.let { | ||||
|                     putString( | ||||
|                         "autosearch", | ||||
|  | @ -49,18 +50,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | |||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         fun pushSync( | ||||
|             activity: Activity?, | ||||
|             autoSearch: String? = null, | ||||
|             callback: (SearchClickCallback) -> Unit | ||||
|         ) { | ||||
|             clickCallback = callback | ||||
|             activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { | ||||
|                 putBoolean("mainapi", false) | ||||
|                 putString("autosearch", autoSearch) | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         var clickCallback: ((SearchClickCallback) -> Unit)? = null | ||||
|     } | ||||
| 
 | ||||
|  | @ -87,10 +76,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | |||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         context?.fixPaddingStatusbar(quick_search_root) | ||||
| 
 | ||||
|         arguments?.getBoolean("mainapi", true)?.let { | ||||
|             isMainApis = it | ||||
|         } | ||||
| 
 | ||||
|         val listLock = ReentrantLock() | ||||
|         observe(searchViewModel.currentSearch) { list -> | ||||
|             try { | ||||
|  | @ -114,45 +99,39 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | |||
| 
 | ||||
|         val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = | ||||
|             ParentItemAdapter(mutableListOf(), { callback -> | ||||
|                 when (callback.action) { | ||||
|                     SEARCH_ACTION_LOAD -> { | ||||
|                         if (isMainApis) { | ||||
|                             activity?.popCurrentPage() | ||||
| 
 | ||||
|                 SearchHelper.handleSearchClickCallback(activity, callback) | ||||
|                         } else { | ||||
|                             clickCallback?.invoke(callback) | ||||
|                         } | ||||
|                     } | ||||
|                     else -> SearchHelper.handleSearchClickCallback(activity, callback) | ||||
|                 } | ||||
|                 //when (callback.action) { | ||||
|                     //SEARCH_ACTION_LOAD -> { | ||||
|                     //    clickCallback?.invoke(callback) | ||||
|                     //} | ||||
|                 //    else -> SearchHelper.handleSearchClickCallback(activity, callback) | ||||
|                 //} | ||||
|             }, { item -> | ||||
|                 activity?.loadHomepageList(item) | ||||
|             }) | ||||
| 
 | ||||
|         val searchExitIcon = | ||||
|             quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) | ||||
| 
 | ||||
|         val searchMagIcon = | ||||
|             quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon) | ||||
| 
 | ||||
|         searchMagIcon?.scaleX = 0.65f | ||||
|         searchMagIcon?.scaleY = 0.65f | ||||
| 
 | ||||
|         quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { | ||||
|             override fun onQueryTextSubmit(query: String): Boolean { | ||||
|                 val active = if (isMainApis) { | ||||
|                     val langs = context?.getApiProviderLangSettings() | ||||
|                     apis.filter { langs?.contains(it.lang) == true }.map { it.name }.toSet() | ||||
|                 } else emptySet() | ||||
| 
 | ||||
|                 context?.filterProviderByPreferredMedia(hasHomePageIsRequired = false) | ||||
|                     ?.map { it.name }?.toSet()?.let { active -> | ||||
|                     searchViewModel.searchAndCancel( | ||||
|                         query = query, | ||||
|                     isMainApis = isMainApis, | ||||
|                         ignoreSettings = false, | ||||
|                         providersActive = active | ||||
|                     ) | ||||
|                     quick_search?.let { | ||||
|                         UIHelper.hideKeyboard(it) | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 return true | ||||
|             } | ||||
|  |  | |||
|  | @ -47,8 +47,6 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | |||
| import com.lagradost.cloudstream3.CommonActivity.getCastSession | ||||
| import com.lagradost.cloudstream3.CommonActivity.showToast | ||||
| import com.lagradost.cloudstream3.mvvm.* | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis | ||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||
| import com.lagradost.cloudstream3.ui.WatchType | ||||
| import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD | ||||
| import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO | ||||
|  | @ -60,6 +58,7 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData | |||
| import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment | ||||
| import com.lagradost.cloudstream3.ui.search.SearchAdapter | ||||
| import com.lagradost.cloudstream3.ui.search.SearchHelper | ||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment | ||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings | ||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings | ||||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1 | ||||
|  | @ -73,8 +72,6 @@ import com.lagradost.cloudstream3.utils.CastHelper.startCast | |||
| import com.lagradost.cloudstream3.utils.Coroutines.main | ||||
| import com.lagradost.cloudstream3.utils.DataStore.getFolderName | ||||
| import com.lagradost.cloudstream3.utils.DataStore.setKey | ||||
| import com.lagradost.cloudstream3.utils.DataStoreHelper.addSync | ||||
| import com.lagradost.cloudstream3.utils.DataStoreHelper.getSync | ||||
| import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos | ||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.checkWrite | ||||
|  | @ -93,6 +90,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur | |||
| import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename | ||||
| import kotlinx.android.synthetic.main.fragment_result.* | ||||
| import kotlinx.android.synthetic.main.fragment_result_swipe.* | ||||
| import kotlinx.android.synthetic.main.result_sync.* | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.Job | ||||
| import kotlinx.coroutines.withContext | ||||
|  | @ -372,6 +370,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|     private var currentLoadingCount = | ||||
|         0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED | ||||
|     private lateinit var viewModel: ResultViewModel //by activityViewModels() | ||||
|     private lateinit var syncModel: SyncViewModel | ||||
|     private var currentHeaderName: String? = null | ||||
|     private var currentType: TvType? = null | ||||
|     private var currentEpisodes: List<ResultEpisode>? = null | ||||
|  | @ -384,6 +383,9 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|     ): View? { | ||||
|         viewModel = | ||||
|             ViewModelProvider(this)[ResultViewModel::class.java] | ||||
|         syncModel = | ||||
|             ViewModelProvider(this)[SyncViewModel::class.java] | ||||
| 
 | ||||
|         return inflater.inflate(R.layout.fragment_result_swipe, container, false) | ||||
|     } | ||||
| 
 | ||||
|  | @ -469,16 +471,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|     var startAction: Int? = null | ||||
|     private var startValue: Int? = null | ||||
| 
 | ||||
|     private fun updateSync(id: Int) { | ||||
|         val syncList = getSync(id, SyncApis.map { it.idPrefix }) ?: return | ||||
|         val list = ArrayList<Pair<SyncAPI, String>>() | ||||
|         for (i in 0 until SyncApis.count()) { | ||||
|             val res = syncList[i] ?: continue | ||||
|             list.add(Pair(SyncApis[i], res)) | ||||
|         } | ||||
|         viewModel.updateSync(context, list) | ||||
|     } | ||||
| 
 | ||||
|     private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) { | ||||
|         // java.util.IllegalFormatConversionException: f != java.lang.Integer | ||||
|         // This can fail with malformed formatting | ||||
|  | @ -525,6 +517,16 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|         setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f)) | ||||
|     } | ||||
| 
 | ||||
|     private fun setMalSync(id: String?): Boolean { | ||||
|         syncModel.setMalId(id ?: return false) | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     private fun setAniListSync(id: String?): Boolean { | ||||
|         syncModel.setAniListId(id ?: return false) | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     private fun setActors(actors: List<ActorData>?) { | ||||
|         if (actors.isNullOrEmpty()) { | ||||
|             result_cast_text?.isVisible = false | ||||
|  | @ -1147,6 +1149,45 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         observe(syncModel.status) { status -> | ||||
|             var closed = false | ||||
|             when (status) { | ||||
|                 is Resource.Failure -> { | ||||
|                     result_sync_loading_shimmer?.stopShimmer() | ||||
|                     result_sync_loading_shimmer?.isVisible = false | ||||
|                     result_sync_holder?.isVisible = false | ||||
|                 } | ||||
|                 is Resource.Loading -> { | ||||
|                     result_sync_loading_shimmer?.startShimmer() | ||||
|                     result_sync_loading_shimmer?.isVisible = true | ||||
|                     result_sync_holder?.isVisible = false | ||||
|                 } | ||||
|                 is Resource.Success -> { | ||||
|                     result_sync_loading_shimmer?.stopShimmer() | ||||
|                     result_sync_loading_shimmer?.isVisible = false | ||||
|                     result_sync_holder?.isVisible = true | ||||
| 
 | ||||
|                     val d = status.value | ||||
|                     result_sync_rating?.value = d.score?.toFloat() ?: 0.0f | ||||
| 
 | ||||
|                     /*when(d.status) { | ||||
|                         -1 -> None | ||||
|                         0 -> Watching | ||||
|                         1 -> Completed | ||||
|                         2 -> OnHold | ||||
|                         3 -> Dropped | ||||
|                         4 -> PlanToWatch | ||||
|                         5 -> ReWatching | ||||
|                     }*/ | ||||
|                     //d.status | ||||
|                 } | ||||
|                 null -> { | ||||
|                     closed = false | ||||
|                 } | ||||
|             } | ||||
|             result_overlapping_panels?.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) | ||||
|         } | ||||
| 
 | ||||
|         observe(viewModel.episodes) { episodeList -> | ||||
|             lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this | ||||
|             var isSeriesVisible = false | ||||
|  | @ -1297,7 +1338,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         result_episode_select.setOnClickListener { | ||||
|         result_episode_select?.setOnClickListener { | ||||
|             val ranges = episodeRanges | ||||
|             if (ranges != null) { | ||||
|                 it.popupMenuNoIconsAndNoStringRes(ranges.mapIndexed { index, s -> Pair(index, s) } | ||||
|  | @ -1307,6 +1348,13 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         result_sync_set_score?.setOnClickListener { | ||||
|             // TODO set score | ||||
|             //syncModel.setScore(SyncAPI.SyncStatus( | ||||
|             //    status = | ||||
|             //)) | ||||
|         } | ||||
| 
 | ||||
|         observe(viewModel.publicEpisodesCount) { count -> | ||||
|             if (count < 0) { | ||||
|                 result_episodes_text?.isVisible = false | ||||
|  | @ -1321,19 +1369,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|             currentId = it | ||||
|         } | ||||
| 
 | ||||
|         observe(viewModel.sync) { sync -> | ||||
|             for (s in sync) { | ||||
|                 when (s) { | ||||
|                     is Resource.Success -> { | ||||
|                         val d = s.value ?: continue | ||||
|                         setDuration(d.duration) | ||||
|                         setRating(d.publicScore) | ||||
|                     } | ||||
|                     else -> Unit | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         observe(viewModel.resultResponse) { data -> | ||||
|             when (data) { | ||||
|                 is Resource.Success -> { | ||||
|  | @ -1399,27 +1434,12 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         updateSync(d.getId()) | ||||
|                         result_add_sync?.setOnClickListener { | ||||
|                             QuickSearchFragment.pushSync(activity, d.name) { click -> | ||||
|                                 addSync(d.getId(), click.card.apiName, click.card.url) | ||||
| 
 | ||||
|                                 showToast( | ||||
|                                     activity, | ||||
|                                     context?.getString(R.string.added_sync_format) | ||||
|                                         ?.format(click.card.name), | ||||
|                                     Toast.LENGTH_SHORT | ||||
|                                 ) | ||||
| 
 | ||||
|                                 updateSync(d.getId()) | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         val showStatus = when (d) { | ||||
|                             is TvSeriesLoadResponse -> d.showStatus | ||||
|                             is AnimeLoadResponse -> d.showStatus | ||||
|                             else -> null | ||||
|                         } | ||||
| 
 | ||||
|                         setShow(showStatus) | ||||
|                         setDuration(d.duration) | ||||
|                         setYear(d.year) | ||||
|  | @ -1427,6 +1447,18 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|                         setRecommendations(d.recommendations) | ||||
|                         setActors(d.actors) | ||||
| 
 | ||||
|                         if (SettingsFragment.accountEnabled) | ||||
|                             if (d is AnimeLoadResponse) { | ||||
|                                 if ( | ||||
|                                     setMalSync(d.malId?.toString()) | ||||
|                                     || | ||||
|                                     setAniListSync(d.anilistId?.toString()) | ||||
|                                 ) { | ||||
|                                     syncModel.updateMetadata() | ||||
|                                     syncModel.updateStatus() | ||||
|                                 } | ||||
|                             } | ||||
| 
 | ||||
|                         result_meta_site?.text = d.apiName | ||||
| 
 | ||||
|                         val posterImageLink = d.posterUrl | ||||
|  |  | |||
|  | @ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.getId | |||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||
| import com.lagradost.cloudstream3.mvvm.Resource | ||||
| import com.lagradost.cloudstream3.mvvm.safeApiCall | ||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||
| import com.lagradost.cloudstream3.ui.APIRepository | ||||
| import com.lagradost.cloudstream3.ui.WatchType | ||||
| import com.lagradost.cloudstream3.ui.player.IGenerator | ||||
|  | @ -79,9 +78,6 @@ class ResultViewModel : ViewModel() { | |||
|     private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData() | ||||
|     val watchStatus: LiveData<WatchType> get() = _watchStatus | ||||
| 
 | ||||
|     private val _sync: MutableLiveData<List<Resource<SyncAPI.SyncResult?>>> = MutableLiveData() | ||||
|     val sync: LiveData<List<Resource<SyncAPI.SyncResult?>>> get() = _sync | ||||
| 
 | ||||
|     fun updateWatchStatus(status: WatchType) = viewModelScope.launch { | ||||
|         val currentId = id.value ?: return@launch | ||||
|         _watchStatus.postValue(status) | ||||
|  | @ -233,17 +229,6 @@ class ResultViewModel : ViewModel() { | |||
|         return generator | ||||
|     } | ||||
| 
 | ||||
|     fun updateSync(context: Context?, sync: List<Pair<SyncAPI, String>>) = viewModelScope.launch { | ||||
|         if (context == null) return@launch | ||||
| 
 | ||||
|         val list = ArrayList<Resource<SyncAPI.SyncResult?>>() | ||||
|         for (s in sync) { | ||||
|             val result = safeApiCall { s.first.getResult(s.second) } | ||||
|             list.add(result) | ||||
|             _sync.postValue(list) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun updateEpisodes(localId: Int?, list: List<ResultEpisode>, selection: Int?) { | ||||
|         _episodes.postValue(list) | ||||
|         generator = RepoLinkGenerator(list) | ||||
|  |  | |||
|  | @ -0,0 +1,79 @@ | |||
| package com.lagradost.cloudstream3.ui.result | ||||
| 
 | ||||
| import androidx.lifecycle.LiveData | ||||
| import androidx.lifecycle.MutableLiveData | ||||
| import androidx.lifecycle.ViewModel | ||||
| import androidx.lifecycle.viewModelScope | ||||
| import com.lagradost.cloudstream3.mvvm.Resource | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi | ||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||
| import kotlinx.coroutines.launch | ||||
| 
 | ||||
| class SyncViewModel : ViewModel() { | ||||
|     private val repos = SyncApis | ||||
| 
 | ||||
|     private val _metaResponse: MutableLiveData<Resource<SyncAPI.SyncResult>> = | ||||
|         MutableLiveData() | ||||
| 
 | ||||
|     val metadata: LiveData<Resource<SyncAPI.SyncResult>> get() = _metaResponse | ||||
| 
 | ||||
|     private val _statusResponse: MutableLiveData<Resource<SyncAPI.SyncStatus>?> = | ||||
|         MutableLiveData(null) | ||||
| 
 | ||||
|     val status: LiveData<Resource<SyncAPI.SyncStatus>?> get() = _statusResponse | ||||
| 
 | ||||
|     // prefix, id | ||||
|     private val syncIds = hashMapOf<String, String>() | ||||
| 
 | ||||
|     fun setMalId(id: String) { | ||||
|         syncIds[malApi.idPrefix] = id | ||||
|     } | ||||
| 
 | ||||
|     fun setAniListId(id: String) { | ||||
|         syncIds[aniListApi.idPrefix] = id | ||||
|     } | ||||
| 
 | ||||
|     fun setScore(status: SyncAPI.SyncStatus) = viewModelScope.launch { | ||||
|         for ((prefix, id) in syncIds) { | ||||
|             repos.firstOrNull { it.idPrefix == prefix }?.score(id, status) | ||||
|         } | ||||
| 
 | ||||
|         updateStatus() | ||||
|     } | ||||
| 
 | ||||
|     fun updateStatus() = viewModelScope.launch { | ||||
|         _statusResponse.postValue(Resource.Loading()) | ||||
|         var lastError: Resource<SyncAPI.SyncStatus> = Resource.Failure(false, null, null, "No data") | ||||
|         for ((prefix, id) in syncIds) { | ||||
|             repos.firstOrNull { it.idPrefix == prefix }?.let { | ||||
|                 val result = it.getStatus(id) | ||||
|                 if (result is Resource.Success) { | ||||
|                     _statusResponse.postValue(result) | ||||
|                     return@launch | ||||
|                 } else if (result is Resource.Failure) { | ||||
|                     lastError = result | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         _statusResponse.postValue(lastError) | ||||
|     } | ||||
| 
 | ||||
|     fun updateMetadata() = viewModelScope.launch { | ||||
|         _metaResponse.postValue(Resource.Loading()) | ||||
|         var lastError: Resource<SyncAPI.SyncResult> = Resource.Failure(false, null, null, "No data") | ||||
|         for ((prefix, id) in syncIds) { | ||||
|             repos.firstOrNull { it.idPrefix == prefix }?.let { | ||||
|                 val result = it.getResult(id) | ||||
|                 if (result is Resource.Success) { | ||||
|                     _metaResponse.postValue(result) | ||||
|                     return@launch | ||||
|                 } else if (result is Resource.Failure) { | ||||
|                     lastError = result | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         _metaResponse.postValue(lastError) | ||||
|     } | ||||
| } | ||||
|  | @ -4,15 +4,13 @@ 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.apis | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||
| import com.lagradost.cloudstream3.SearchResponse | ||||
| import com.lagradost.cloudstream3.apmap | ||||
| import com.lagradost.cloudstream3.mvvm.Resource | ||||
| import com.lagradost.cloudstream3.mvvm.safeApiCall | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis | ||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||
| import com.lagradost.cloudstream3.ui.APIRepository | ||||
| import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | ||||
| import kotlinx.coroutines.Dispatchers | ||||
|  | @ -39,7 +37,6 @@ class SearchViewModel : ViewModel() { | |||
|     val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory | ||||
| 
 | ||||
|     private val repos = apis.map { APIRepository(it) } | ||||
|     private val syncApis = SyncApis | ||||
| 
 | ||||
|     fun clearSearch() { | ||||
|         _searchResponse.postValue(Resource.Success(ArrayList())) | ||||
|  | @ -49,33 +46,11 @@ class SearchViewModel : ViewModel() { | |||
|     private var onGoingSearch: Job? = null | ||||
|     fun searchAndCancel( | ||||
|         query: String, | ||||
|         isMainApis: Boolean = true, | ||||
|         providersActive: Set<String> = setOf(), | ||||
|         ignoreSettings: Boolean = false | ||||
|     ) { | ||||
|         onGoingSearch?.cancel() | ||||
|         onGoingSearch = search(query, isMainApis, providersActive, ignoreSettings) | ||||
|     } | ||||
| 
 | ||||
|     data class SyncSearchResultSearchResponse( | ||||
|         override val name: String, | ||||
|         override val url: String, | ||||
|         override val apiName: String, | ||||
|         override var type: TvType?, | ||||
|         override var posterUrl: String?, | ||||
|         override var id: Int?, | ||||
|         override var quality: SearchQuality? = null | ||||
|     ) : SearchResponse | ||||
| 
 | ||||
|     private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse { | ||||
|         return SyncSearchResultSearchResponse( | ||||
|             this.name, | ||||
|             this.url, | ||||
|             this.syncApiName, | ||||
|             null, | ||||
|             this.posterUrl, | ||||
|             null, //this.id.hashCode() | ||||
|         ) | ||||
|         onGoingSearch = search(query, providersActive, ignoreSettings) | ||||
|     } | ||||
| 
 | ||||
|     fun updateHistory() = viewModelScope.launch { | ||||
|  | @ -89,7 +64,6 @@ class SearchViewModel : ViewModel() { | |||
| 
 | ||||
|     private fun search( | ||||
|         query: String, | ||||
|         isMainApis: Boolean = true, | ||||
|         providersActive: Set<String>, | ||||
|         ignoreSettings: Boolean = false | ||||
|     ) = | ||||
|  | @ -118,7 +92,6 @@ class SearchViewModel : ViewModel() { | |||
|             _currentSearch.postValue(ArrayList()) | ||||
| 
 | ||||
|             withContext(Dispatchers.IO) { // This interrupts UI otherwise | ||||
|                 if (isMainApis) { | ||||
|                 repos.filter { a -> | ||||
|                     ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name)) | ||||
|                 }.apmap { a -> // Parallel | ||||
|  | @ -126,16 +99,6 @@ class SearchViewModel : ViewModel() { | |||
|                     currentList.add(OnGoingSearch(a.name, search)) | ||||
|                     _currentSearch.postValue(currentList) | ||||
|                 } | ||||
|                 } else { | ||||
|                     syncApis.apmap { a -> | ||||
|                         val search = safeApiCall { | ||||
|                             a.search(query)?.map { it.toSearchResponse() } | ||||
|                                 ?: throw ErrorLoadingException() | ||||
|                         } | ||||
| 
 | ||||
|                         currentList.add(OnGoingSearch(a.name, search)) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             _currentSearch.postValue(currentList) | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,30 @@ | |||
| package com.lagradost.cloudstream3.ui.search | ||||
| 
 | ||||
| import com.lagradost.cloudstream3.SearchQuality | ||||
| import com.lagradost.cloudstream3.SearchResponse | ||||
| import com.lagradost.cloudstream3.TvType | ||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||
| 
 | ||||
| class SyncSearchViewModel { | ||||
|     data class SyncSearchResultSearchResponse( | ||||
|         override val name: String, | ||||
|         override val url: String, | ||||
|         override val apiName: String, | ||||
|         override var type: TvType?, | ||||
|         override var posterUrl: String?, | ||||
|         override var id: Int?, | ||||
|         override var quality: SearchQuality? = null | ||||
|     ) : SearchResponse | ||||
| 
 | ||||
|     private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse { | ||||
|         return SyncSearchResultSearchResponse( | ||||
|             this.name, | ||||
|             this.url, | ||||
|             this.syncApiName, | ||||
|             null, | ||||
|             this.posterUrl, | ||||
|             null, //this.id.hashCode() | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -88,7 +88,7 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|             return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION | ||||
|         } | ||||
| 
 | ||||
|         private const val accountEnabled = false | ||||
|         const val accountEnabled = false | ||||
|     } | ||||
| 
 | ||||
|     private var beneneCount = 0 | ||||
|  | @ -155,7 +155,11 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|         val dialog = builder.show() | ||||
| 
 | ||||
|         dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener { | ||||
|             try { | ||||
|                 api.authenticate() | ||||
|             } catch (e: Exception) { | ||||
|                 logError(e) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         val ogIndex = api.accountIndex | ||||
|  | @ -325,10 +329,9 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
| 
 | ||||
|         val syncApis = | ||||
|             listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi)) | ||||
|         for (sync in syncApis) { | ||||
|             getPref(sync.first)?.apply { | ||||
|         for ((key, api) in syncApis) { | ||||
|             getPref(key)?.apply { | ||||
|                 isVisible = accountEnabled | ||||
|                 val api = sync.second | ||||
|                 title = | ||||
|                     getString(R.string.login_format).format(api.name, getString(R.string.account)) | ||||
|                 setOnPreferenceClickListener { _ -> | ||||
|  | @ -336,7 +339,11 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|                     if (info != null) { | ||||
|                         showLoginInfo(api, info) | ||||
|                     } else { | ||||
|                         try { | ||||
|                             api.authenticate() | ||||
|                         } catch (e: Exception) { | ||||
|                             logError(e) | ||||
|                         } | ||||
|                     } | ||||
|                     return@setOnPreferenceClickListener true | ||||
|                 } | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ package com.lagradost.cloudstream3.utils | |||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.Activity | ||||
| import android.content.ComponentName | ||||
| import android.content.ContentValues | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
|  | @ -23,6 +22,7 @@ import android.util.Log | |||
| import androidx.annotation.RequiresApi | ||||
| import androidx.annotation.WorkerThread | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.tvprovider.media.tv.PreviewChannelHelper | ||||
| import androidx.tvprovider.media.tv.TvContractCompat | ||||
| import androidx.tvprovider.media.tv.WatchNextProgram | ||||
|  | @ -33,7 +33,10 @@ import com.google.android.gms.cast.framework.CastState | |||
| import com.google.android.gms.common.ConnectionResult | ||||
| import com.google.android.gms.common.GoogleApiAvailability | ||||
| import com.google.android.gms.common.wrappers.Wrappers | ||||
| import com.lagradost.cloudstream3.* | ||||
| import com.lagradost.cloudstream3.R | ||||
| import com.lagradost.cloudstream3.SearchResponse | ||||
| import com.lagradost.cloudstream3.isMovieType | ||||
| import com.lagradost.cloudstream3.mapper | ||||
| import com.lagradost.cloudstream3.mvvm.logError | ||||
| import com.lagradost.cloudstream3.ui.result.ResultFragment | ||||
| import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | ||||
|  | @ -196,17 +199,10 @@ object AppUtils { | |||
| 
 | ||||
|     fun Context.openBrowser(url: String) { | ||||
|         try { | ||||
|             val components = arrayOf(ComponentName(applicationContext, MainActivity::class.java)) | ||||
|             val intent = Intent(Intent.ACTION_VIEW) | ||||
|             intent.data = Uri.parse(url) | ||||
| 
 | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) | ||||
|                 startActivity( | ||||
|                     Intent.createChooser(intent, null) | ||||
|                         .putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components) | ||||
|                 ) | ||||
|             else | ||||
|                 startActivity(intent) | ||||
|             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) | ||||
|             ContextCompat.startActivity(this, intent, null) | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|         } | ||||
|  |  | |||
|  | @ -7,9 +7,10 @@ | |||
|         android:layout_height="match_parent"> | ||||
| 
 | ||||
|     <LinearLayout | ||||
|             android:visibility="gone" | ||||
|             android:padding="16dp" | ||||
|             android:orientation="vertical" | ||||
|             android:id="@+id/sync_holder" | ||||
|             android:id="@+id/result_sync_holder" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="match_parent"> | ||||
| 
 | ||||
|  | @ -38,8 +39,9 @@ | |||
|                     app:tint="?attr/textColor" /> | ||||
| 
 | ||||
|             <EditText | ||||
|                     android:id="@+id/result_sync_current_episodes" | ||||
|                     style="@style/AppEditStyle" | ||||
|                     android:hint="20" | ||||
|                     tools:hint="20" | ||||
|                     android:textSize="20sp" | ||||
|                     android:inputType="number" | ||||
|                     android:layout_width="wrap_content" | ||||
|  | @ -47,11 +49,12 @@ | |||
|                     tools:ignore="LabelFor" /> | ||||
| 
 | ||||
|             <TextView | ||||
|                     android:id="@+id/result_sync_max_episodes" | ||||
|                     android:layout_gravity="center_vertical" | ||||
|                     android:paddingBottom="1dp" | ||||
|                     android:textSize="20sp" | ||||
|                     android:textColor="?attr/textColor" | ||||
|                     android:text="/30" | ||||
|                     tools:text="/30" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" /> | ||||
| 
 | ||||
|  | @ -64,10 +67,10 @@ | |||
|                     android:layout_gravity="end|center_vertical" | ||||
|                     android:contentDescription="@string/result_share" | ||||
|                     app:tint="?attr/textColor" /> | ||||
| 
 | ||||
|         </LinearLayout> | ||||
| 
 | ||||
|         <androidx.core.widget.ContentLoadingProgressBar | ||||
|                 android:id="@+id/result_sync_episodes" | ||||
|                 android:padding="10dp" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="20dp" | ||||
|  | @ -192,6 +195,7 @@ | |||
|         </LinearLayout> | ||||
| 
 | ||||
|         <com.google.android.material.slider.Slider | ||||
|                 android:id="@+id/result_sync_rating" | ||||
|                 android:valueFrom="0" | ||||
|                 android:valueTo="10" | ||||
|                 android:value="4" | ||||
|  | @ -350,6 +354,7 @@ | |||
|                 style="@style/WhiteButton" | ||||
|                 android:text="@string/type_watching" /> | ||||
|         <com.google.android.material.button.MaterialButton | ||||
|                 android:id="@+id/result_sync_set_score" | ||||
|                 android:layout_marginTop="10dp" | ||||
|                 android:layout_width="match_parent" | ||||
|                 style="@style/BlackButton" | ||||
|  | @ -357,13 +362,48 @@ | |||
|                 android:text="@string/upload_sync" /> | ||||
|     </LinearLayout> | ||||
| 
 | ||||
| 
 | ||||
|     <com.google.android.material.button.MaterialButton | ||||
|             android:visibility="gone" | ||||
|             android:id="@+id/sync_add_tracking" | ||||
|             android:layout_gravity="center" | ||||
|     <com.facebook.shimmer.ShimmerFrameLayout | ||||
|             android:id="@+id/result_sync_loading_shimmer" | ||||
|             app:shimmer_base_alpha="0.2" | ||||
|             app:shimmer_highlight_alpha="0.3" | ||||
|             app:shimmer_duration="@integer/loading_time" | ||||
|             app:shimmer_auto_start="true" | ||||
|             android:padding="15dp" | ||||
|             android:layout_width="match_parent" | ||||
|             style="@style/SyncButton" | ||||
|             app:icon="@drawable/ic_baseline_add_24" | ||||
|             android:text="@string/add_sync" /> | ||||
|             android:layout_height="match_parent" | ||||
|             android:layout_gravity="center" | ||||
|             android:orientation="vertical"> | ||||
| 
 | ||||
|         <LinearLayout | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:orientation="vertical"> | ||||
|             <Space | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="30dp"/> | ||||
|             <include layout="@layout/loading_line_short" /> | ||||
| 
 | ||||
|             <include layout="@layout/loading_line" /> | ||||
|             <Space | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="30dp"/> | ||||
|             <include layout="@layout/loading_line_short" /> | ||||
|             <include layout="@layout/loading_line" /> | ||||
|             <Space | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="30dp"/> | ||||
|             <include layout="@layout/loading_line_short" /> | ||||
|             <include layout="@layout/loading_line_short" /> | ||||
|             <include layout="@layout/loading_line_short" /> | ||||
|             <include layout="@layout/loading_line_short" /> | ||||
|             <include layout="@layout/loading_line_short" /> | ||||
|             <Space | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="30dp"/> | ||||
|             <include layout="@layout/loading_line" /> | ||||
|             <include layout="@layout/loading_line" /> | ||||
| 
 | ||||
|         </LinearLayout> | ||||
|     </com.facebook.shimmer.ShimmerFrameLayout> | ||||
| 
 | ||||
| </FrameLayout> | ||||
|  | @ -1,5 +1,6 @@ | |||
| package com.lagradost.cloudstream3 | ||||
| 
 | ||||
| import com.lagradost.cloudstream3.APIHolder.allProviders | ||||
| import com.lagradost.cloudstream3.mvvm.logError | ||||
| import com.lagradost.cloudstream3.utils.Qualities | ||||
| import com.lagradost.cloudstream3.utils.SubtitleHelper | ||||
|  | @ -9,9 +10,7 @@ import org.junit.Test | |||
| 
 | ||||
| class ProviderTests { | ||||
|     private fun getAllProviders(): List<MainAPI> { | ||||
|         val allApis = APIHolder.apis | ||||
|         allApis.addAll(APIHolder.restrictedApis) | ||||
|         return allApis.filter { !it.usesWebView } | ||||
|         return allProviders.filter { !it.usesWebView } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun loadLinks(api: MainAPI, url: String?): Boolean { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue