diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml index 28b737b3..2a54857c 100644 --- a/.github/workflows/issue_action.yml +++ b/.github/workflows/issue_action.yml @@ -53,6 +53,18 @@ jobs: Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). Found provider name: `${{ steps.provider_check.outputs.name }}` + - name: Label if mentions provider + if: steps.provider_check.outputs.name != 'none' + uses: actions/github-script@v6 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible provider issue"] + }) - name: Add eyes reaction to all issues uses: actions-cool/emoji-helper@v1.0.0 with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1cbcec68..3c855d28 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,8 +47,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 56 - versionName = "3.5.0" + versionCode = 57 + versionName = "4.0.0" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") @@ -190,7 +190,7 @@ dependencies { // Networking // implementation("com.squareup.okhttp3:okhttp:4.9.2") // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") - implementation("com.github.Blatzar:NiceHttp:0.4.1") + implementation("com.github.Blatzar:NiceHttp:0.4.2") // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 @@ -220,6 +220,9 @@ dependencies { // Library/extensions searching with Levenshtein distance implementation("me.xdrop:fuzzywuzzy:1.4.0") + + // color pallette for images -> colors + implementation("androidx.palette:palette-ktx:1.0.0") } tasks.register("androidSourcesJar", Jar::class) { @@ -250,4 +253,4 @@ tasks.withType().configureEach { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 8c818027..73859021 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -13,7 +13,10 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf @@ -510,6 +513,20 @@ abstract class MainAPI { open val hasMainPage = false open val hasQuickSearch = false + /** + * A set of which ids the provider can open with getLoadUrl() + * If the set contains SyncIdName.Imdb then getLoadUrl() can be started with + * an Imdb class which inherits from SyncId. + * + * getLoadUrl() is then used to get page url based on that ID. + * + * Example: + * "tt6723592" -> getLoadUrl(ImdbSyncId("tt6723592")) -> "mainUrl/imdb/tt6723592" -> load("mainUrl/imdb/tt6723592") + * + * This is used to launch pages from personal lists or recommendations using IDs. + **/ + open val supportedSyncNames = setOf() + open val supportedTypes = setOf( TvType.Movie, TvType.TvSeries, @@ -580,6 +597,14 @@ abstract class MainAPI { open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? { return null } + + /** + * Get the load() url based on a sync ID like IMDb or MAL. + * Only contains SyncIds based on supportedSyncUrls. + **/ + open suspend fun getLoadUrl(name: SyncIdName, id: String): String? { + return null + } } /** Might need a different implementation for desktop*/ diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 857eaa6a..eddec15e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -347,7 +347,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } - var lastPopup : SearchResponse? = null + var lastPopup: SearchResponse? = null fun loadPopup(result: SearchResponse) { lastPopup = result viewModel.load( @@ -388,6 +388,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val isNavVisible = listOf( R.id.navigation_home, R.id.navigation_search, + R.id.navigation_library, R.id.navigation_downloads, R.id.navigation_settings, R.id.navigation_download_child, @@ -438,6 +439,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { nav_view?.isVisible = isNavVisible && !landscape nav_rail_view?.isVisible = isNavVisible && landscape + + // Hide library on TV since it is not supported yet :( + val isTrueTv = isTrueTvSettings() + nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv } //private var mCastSession: CastSession? = null @@ -710,7 +716,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { changeStatusBarState(isEmulatorSettings()) - if (lastError == null) { + + if (PluginManager.checkSafeModeFile()) { + normalSafeApiCall { + showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG) + } + } else if (lastError == null) { ioSafe { getKey(USER_SELECTED_HOMEPAGE_API)?.let { homeApi -> mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt index 8e3dc730..bc910a7e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -1,32 +1,51 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import java.net.URI + +class FileMoon : Filesim() { + override val mainUrl = "https://filemoon.to" + override val name = "FileMoon" +} open class Filesim : ExtractorApi() { override val name = "Filesim" override val mainUrl = "https://files.im" override val requiresReferer = false - override suspend fun getUrl(url: String, referer: String?): List { - val sources = mutableListOf() + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { with(app.get(url).document) { - this.select("script").map { script -> + this.select("script").forEach { script -> if (script.data().contains("eval(function(p,a,c,k,e,d)")) { - val data = getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]") - tryParseJson>("[$data]")?.map { - M3u8Helper.generateM3u8( - name, - it.file, - "$mainUrl/", - ).forEach { m3uData -> sources.add(m3uData) } + val data = getAndUnpack(script.data()) + val foundData = Regex("""sources:\[(.*?)]""").find(data)?.groupValues?.get(1) ?: return@forEach + val fixedData = foundData.replace("file:", """"file":""") + + parseJson>("[$fixedData]").forEach { + callback.invoke( + ExtractorLink( + name, + name, + it.file, + "$mainUrl/", + Qualities.Unknown.value, + URI(it.file).path.endsWith(".m3u8") + ) + ) } } } } - return sources } private data class ResponseSource( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt index f25cb5ba..2adc00d5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt @@ -6,6 +6,11 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* +class Vanfem : GuardareStream() { + override var name = "Vanfem" + override var mainUrl = "https://vanfem.com/" +} + class CineGrabber : GuardareStream() { override var name = "CineGrabber" override var mainUrl = "https://cinegrabber.com" diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt index b910f9dd..a27bf188 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt @@ -59,8 +59,8 @@ open class VidSrcExtractor : ExtractorApi() { if (datahash.isNotBlank()) { val links = try { app.get( - "$absoluteUrl/src/$datahash", - referer = "https://source.vidsrc.me/" + "$absoluteUrl/srcrcp/$datahash", + referer = "https://rcp.vidsrc.me/" ).url } catch (e: Exception) { "" @@ -71,7 +71,7 @@ open class VidSrcExtractor : ExtractorApi() { serverslist.amap { server -> val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/") - if (linkfixed.contains("/pro")) { + if (linkfixed.contains("/prorcp")) { val srcresponse = app.get(server, referer = absoluteUrl).text val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)") val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt deleted file mode 100644 index 208db14b..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.lagradost.cloudstream3.metaproviders - -import com.lagradost.cloudstream3.ErrorLoadingException -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi -import com.lagradost.cloudstream3.utils.SyncUtil - -object SyncRedirector { - val syncApis = SyncApis - - suspend fun redirect(url: String, preferredUrl: String): String { - for (api in syncApis) { - if (url.contains(api.mainUrl)) { - val otherApi = when (api.name) { - aniListApi.name -> "anilist" - malApi.name -> "myanimelist" - else -> return url - } - - return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl -> - realUrl.contains(preferredUrl) - } ?: run { - throw ErrorLoadingException("Page does not exist on $preferredUrl") - } - } - } - return url - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt new file mode 100644 index 00000000..75e96bec --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt @@ -0,0 +1,56 @@ +package com.lagradost.cloudstream3.metaproviders + +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis +import com.lagradost.cloudstream3.syncproviders.SyncIdName + +object SyncRedirector { + val syncApis = SyncApis + private val syncIds = + listOf( + SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""), + SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""") + ) + + suspend fun redirect( + url: String, + providerApi: MainAPI + ): String { + // Deprecated since providers should do this instead! + + // Tries built in ID -> ProviderUrl + /* + for (api in syncApis) { + if (url.contains(api.mainUrl)) { + val otherApi = when (api.name) { + aniListApi.name -> "anilist" + malApi.name -> "myanimelist" + else -> return url + } + + SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl -> + realUrl.contains(providerApi.mainUrl) + }?.let { + return it + } +// ?: run { +// throw ErrorLoadingException("Page does not exist on $preferredUrl") +// } + } + } + */ + + // Tries provider solution + // This goes through all sync ids and finds supported id by said provider + return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) -> + if (providerApi.supportedSyncNames.contains(syncName)) { + syncRegex.find(url)?.value?.let { + suspendSafeApiCall { + providerApi.getLoadUrl(syncName, it) + } + } + } else null + } ?: url + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 54fe5d75..3533d6a8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -144,8 +144,10 @@ object PluginManager { return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray() } - private val LOCAL_PLUGINS_PATH = - Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins" + private val CLOUD_STREAM_FOLDER = + Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/" + + private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins" public var currentlyLoading: String? = null @@ -421,6 +423,21 @@ object PluginManager { afterPluginsLoadedEvent.invoke(forceReload) } + /** + * This can be used to override any extension loading to fix crashes! + * @return true if safe mode file is present + **/ + fun checkSafeModeFile(): Boolean { + return normalSafeApiCall { + val folder = File(CLOUD_STREAM_FOLDER) + if (!folder.exists()) return@normalSafeApiCall false + val files = folder.listFiles { _, name -> + name.equals("safe", ignoreCase = true) + } + files?.any() + } ?: false + } + /** * @return True if successful, false if not * */ diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index f09bf8fe..f17086c1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -13,6 +13,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val openSubtitlesApi = OpenSubtitlesApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() + val localListApi = LocalList() // used to login via app intent val OAuth2Apis @@ -29,7 +30,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // used for active syncing val SyncApis get() = listOf( - SyncRepo(malApi), SyncRepo(aniListApi) + SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi) ) val inAppAuths diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index 5aa56a02..8c76c5bf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -1,10 +1,31 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.ui.result.UiText +import me.xdrop.fuzzywuzzy.FuzzySearch + +enum class SyncIdName { + Anilist, + MyAnimeList, + Trakt, + Imdb, + LocalList +} interface SyncAPI : OAuth2API { + /** + * Set this to true if the user updates something on the list like watch status or score + **/ + var requireLibraryRefresh: Boolean val mainUrl: String + /** + * Allows certain providers to open pages from + * library links. + **/ + val syncIdName: SyncIdName + /** -1 -> None 0 -> Watching @@ -22,7 +43,9 @@ interface SyncAPI : OAuth2API { suspend fun search(name: String): List? - fun getIdFromUrl(url : String) : String + suspend fun getPersonalLibrary(): LibraryMetadata? + + fun getIdFromUrl(url: String): String data class SyncSearchResult( override val name: String, @@ -42,7 +65,7 @@ interface SyncAPI : OAuth2API { val score: Int?, val watchedEpisodes: Int?, var isFavorite: Boolean? = null, - var maxEpisodes : Int? = null, + var maxEpisodes: Int? = null, ) data class SyncResult( @@ -63,9 +86,9 @@ interface SyncAPI : OAuth2API { var genres: List? = null, var synonyms: List? = null, var trailers: List? = null, - var isAdult : Boolean? = null, + var isAdult: Boolean? = null, var posterUrl: String? = null, - var backgroundPosterUrl : String? = null, + var backgroundPosterUrl: String? = null, /** In unixtime */ var startDate: Long? = null, @@ -76,4 +99,61 @@ interface SyncAPI : OAuth2API { var prevSeason: SyncSearchResult? = null, var actors: List? = null, ) + + + data class Page( + val title: UiText, var items: List + ) { + fun sort(method: ListSorting?, query: String? = null) { + items = when (method) { + ListSorting.Query -> + if (query != null) { + items.sortedBy { + -FuzzySearch.partialRatio( + query.lowercase(), it.name.lowercase() + ) + } + } else items + ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating ?: 0) } + ListSorting.RatingLow -> items.sortedBy { (it.personalRating ?: 0) } + ListSorting.AlphabeticalA -> items.sortedBy { it.name } + ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed() + ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) } + ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime } + else -> items + } + } + } + + data class LibraryMetadata( + val allLibraryLists: List, + val supportedListSorting: Set + ) + + data class LibraryList( + val name: UiText, + val items: List + ) + + data class LibraryItem( + override val name: String, + override val url: String, + /** + * Unique unchanging string used for data storage. + * This should be the actual id when you change scores and status + * since score changes from library might get added in the future. + **/ + val syncId: String, + val episodesCompleted: Int?, + val episodesTotal: Int?, + /** Out of 100 */ + val personalRating: Int?, + val lastUpdatedUnixTime: Long?, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override var posterHeaders: Map?, + override var quality: SearchQuality?, + override var id: Int? = null, + ) : SearchResponse } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt index b621e81a..85b877e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt @@ -11,26 +11,38 @@ class SyncRepo(private val repo: SyncAPI) { val icon = repo.icon val mainUrl = repo.mainUrl val requiresLogin = repo.requiresLogin + val syncIdName = repo.syncIdName + var requireLibraryRefresh: Boolean + get() = repo.requireLibraryRefresh + set(value) { + repo.requireLibraryRefresh = value + } suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource { return safeApiCall { repo.score(id, status) } } - suspend fun getStatus(id : String) : Resource { + suspend fun getStatus(id: String): Resource { return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } } - suspend fun getResult(id : String) : Resource { + suspend fun getResult(id: String): Resource { return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") } } - suspend fun search(query : String) : Resource> { + suspend fun search(query: String): Resource> { return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() } } - fun hasAccount() : Boolean { + suspend fun getPersonalLibrary(): Resource { + return safeApiCall { repo.getPersonalLibrary() ?: throw ErrorLoadingException() } + } + + fun hasAccount(): Boolean { return normalSafeApiCall { repo.loginInfo() != null } ?: false } - fun getIdFromUrl(url : String) : String = repo.getIdFromUrl(url) + fun getIdFromUrl(url: String): String? = normalSafeApiCall { + repo.getIdFromUrl(url) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index d4742d94..7d9de43a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -1,10 +1,10 @@ package com.lagradost.cloudstream3.syncproviders.providers +import androidx.annotation.StringRes import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.mvvm.logError @@ -12,6 +12,9 @@ import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.toJson @@ -27,10 +30,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override val key = "6871" override val redirectUrl = "anilistlogin" override val idPrefix = "anilist" + override var requireLibraryRefresh = true override var mainUrl = "https://anilist.co" override val icon = R.drawable.ic_anilist_icon override val requiresLogin = false override val createAccountUrl = "$mainUrl/signup" + override val syncIdName = SyncIdName.Anilist override fun loginInfo(): AuthAPI.LoginInfo? { // context.getUser(true)?. @@ -45,6 +50,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } override fun logOut() { + requireLibraryRefresh = true removeAccountKeys() } @@ -64,8 +70,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { switchToNewAccount() setKey(accountId, ANILIST_UNIXTIME_KEY, endTime) setKey(accountId, ANILIST_TOKEN_KEY, token) - setKey(ANILIST_SHOULD_UPDATE_LIST, true) val user = getUser() + requireLibraryRefresh = true return user != null } @@ -140,7 +146,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { this.name, recMedia.id?.toString() ?: return@mapNotNull null, getUrlFromId(recMedia.id), - recMedia.coverImage?.large ?: recMedia.coverImage?.medium + recMedia.coverImage?.extraLarge ?: recMedia.coverImage?.large + ?: recMedia.coverImage?.medium ) }, trailers = when (season.trailer?.site?.lowercase()?.trim()) { @@ -170,7 +177,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { fromIntToAnimeStatus(status.status), status.score, status.watchedEpisodes - ) + ).also { + requireLibraryRefresh = requireLibraryRefresh || it + } } companion object { @@ -181,7 +190,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { const val ANILIST_TOKEN_KEY: String = "anilist_token" // anilist token for api const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile const val ANILIST_CACHED_LIST: String = "anilist_cached_list" - const val ANILIST_SHOULD_UPDATE_LIST: String = "anilist_should_update_list" private fun fixName(name: String): String { return name.lowercase(Locale.ROOT).replace(" ", "") @@ -219,7 +227,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { romaji } idMal - coverImage { medium large } + coverImage { medium large extraLarge } averageScore } } @@ -232,7 +240,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { format id idMal - coverImage { medium large } + coverImage { medium large extraLarge } averageScore title { english @@ -292,15 +300,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val shows = searchShows(name.replace(blackListRegex, "")) shows?.data?.Page?.media?.find { - malId ?: "NONE" == it.idMal.toString() + (malId ?: "NONE") == it.idMal.toString() }?.let { return it } val filtered = shows?.data?.Page?.media?.filter { - ( - it.startDate.year ?: year.toString() == year.toString() - || year == null - ) + (((it.startDate.year ?: year.toString()) == year.toString() + || year == null)) } filtered?.forEach { it.title.romaji?.let { romaji -> @@ -312,14 +318,14 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } // Changing names of these will show up in UI - enum class AniListStatusType(var value: Int) { - Watching(0), - Completed(1), - Paused(2), - Dropped(3), - Planning(4), - ReWatching(5), - None(-1) + enum class AniListStatusType(var value: Int, @StringRes val stringRes: Int) { + Watching(0, R.string.type_watching), + Completed(1, R.string.type_completed), + Paused(2, R.string.type_on_hold), + Dropped(3, R.string.type_dropped), + Planning(4, R.string.type_plan_to_watch), + ReWatching(5, R.string.type_re_watching), + None(-1, R.string.none) } fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp } @@ -335,7 +341,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } } - fun convertAnilistStringToStatus(string: String): AniListStatusType { + fun convertAniListStringToStatus(string: String): AniListStatusType { return fromIntToAnimeStatus(aniListStatusString.indexOf(string)) } @@ -526,7 +532,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { app.post( "https://graphql.anilist.co/", headers = mapOf( - "Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null), + "Authorization" to "Bearer " + (getAuth() + ?: return@suspendSafeApiCall null), if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" ), cacheTime = 0, @@ -575,7 +582,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { data class CoverImage( @JsonProperty("medium") val medium: String?, - @JsonProperty("large") val large: String? + @JsonProperty("large") val large: String?, + @JsonProperty("extraLarge") val extraLarge: String? ) data class Media( @@ -602,7 +610,29 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("score") val score: Int, @JsonProperty("private") val private: Boolean, @JsonProperty("media") val media: Media - ) + ) { + fun toLibraryItem(): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + // English title first + this.media.title.english ?: this.media.title.romaji + ?: this.media.synonyms.firstOrNull() + ?: "", + "https://anilist.co/anime/${this.media.id}/", + this.media.id.toString(), + this.progress, + this.media.episodes, + this.score, + this.updatedAt.toLong(), + "AniList", + TvType.Anime, + this.media.coverImage.extraLarge ?: this.media.coverImage.large + ?: this.media.coverImage.medium, + null, + null, + null + ) + } + } data class Lists( @JsonProperty("status") val status: String?, @@ -617,40 +647,59 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection ) - fun getAnilistListCached(): Array? { + private fun getAniListListCached(): Array? { return getKey(ANILIST_CACHED_LIST) as? Array } - suspend fun getAnilistAnimeListSmart(): Array? { + private suspend fun getAniListAnimeListSmart(): Array? { if (getAuth() == null) return null if (checkToken()) return null - return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) { - val list = getFullAnilistList()?.data?.MediaListCollection?.lists?.toTypedArray() + return if (requireLibraryRefresh) { + val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray() if (list != null) { setKey(ANILIST_CACHED_LIST, list) - setKey(ANILIST_SHOULD_UPDATE_LIST, false) } list } else { - getAnilistListCached() + getAniListListCached() } } - private suspend fun getFullAnilistList(): FullAnilistList? { - var userID: Int? = null - /** WARNING ASSUMES ONE USER! **/ - getKeys(ANILIST_USER_KEY)?.forEach { key -> - getKey(key, null)?.let { - userID = it.id - } - } + override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { + val list = getAniListAnimeListSmart()?.groupBy { + convertAniListStringToStatus(it.status ?: "").stringRes + }?.mapValues { group -> + group.value.map { it.entries.map { entry -> entry.toLibraryItem() } }.flatten() + } ?: emptyMap() - val fixedUserID = userID ?: return null + // To fill empty lists when AniList does not return them + val baseMap = + AniListStatusType.values().filter { it.value >= 0 }.associate { + it.stringRes to emptyList() + } + + return SyncAPI.LibraryMetadata( + (baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) }, + setOf( + ListSorting.AlphabeticalA, + ListSorting.AlphabeticalZ, + ListSorting.UpdatedNew, + ListSorting.UpdatedOld, + ListSorting.RatingHigh, + ListSorting.RatingLow, + ) + ) + } + + private suspend fun getFullAniListList(): FullAnilistList? { + /** WARNING ASSUMES ONE USER! **/ + + val userID = getKey(accountId, ANILIST_USER_KEY)?.id ?: return null val mediaType = "ANIME" val query = """ - query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) { + query (${'$'}userID: Int = $userID, ${'$'}MEDIA: MediaType = $mediaType) { MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) { lists { status @@ -661,7 +710,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { startedAt { year month day } updatedAt progress - score + score (format: POINT_100) private media { @@ -677,7 +726,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { english romaji } - coverImage { medium } + coverImage { extraLarge large medium } synonyms nextAiringEpisode { timeUntilAiring diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt new file mode 100644 index 00000000..0b081220 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -0,0 +1,100 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import androidx.fragment.app.FragmentActivity +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.WatchType +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.utils.Coroutines.ioWork +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds +import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState + +class LocalList : SyncAPI { + override val name = "Local" + override val icon: Int = R.drawable.ic_baseline_storage_24 + override val requiresLogin = false + override val createAccountUrl: Nothing? = null + override val idPrefix = "local" + override var requireLibraryRefresh = true + + override fun loginInfo(): AuthAPI.LoginInfo { + return AuthAPI.LoginInfo( + null, + null, + 0 + ) + } + + override fun logOut() { + + } + + override val key: String = "" + override val redirectUrl = "" + override suspend fun handleRedirect(url: String): Boolean { + return true + } + + override fun authenticate(activity: FragmentActivity?) { + } + + override val mainUrl = "" + override val syncIdName = SyncIdName.LocalList + override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + return true + } + + override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { + return null + } + + override suspend fun getResult(id: String): SyncAPI.SyncResult? { + return null + } + + override suspend fun search(name: String): List? { + return null + } + + override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? { + val watchStatusIds = ioWork { + getAllWatchStateIds()?.map { id -> + Pair(id, getResultWatchState(id)) + } + }?.distinctBy { it.first } ?: return null + + val list = ioWork { + watchStatusIds.groupBy { + it.second.stringRes + }.mapValues { group -> + group.value.mapNotNull { + getBookmarkedData(it.first)?.toLibraryItem(it.first.toString()) + } + } + } + + val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate { + // None is not something to display + it.stringRes to emptyList() + } + return SyncAPI.LibraryMetadata( + (baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) }, + setOf( + ListSorting.AlphabeticalA, + ListSorting.AlphabeticalZ, +// ListSorting.UpdatedNew, +// ListSorting.UpdatedOld, +// ListSorting.RatingHigh, +// ListSorting.RatingLow, + ) + ) + } + + override fun getIdFromUrl(url: String): String { + return url + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index c08958ce..5164b606 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.syncproviders.providers import android.util.Base64 +import androidx.annotation.StringRes import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -8,11 +9,15 @@ 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.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject @@ -31,13 +36,15 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override val redirectUrl = "mallogin" override val idPrefix = "mal" override var mainUrl = "https://myanimelist.net" - val apiUrl = "https://api.myanimelist.net" + private val apiUrl = "https://api.myanimelist.net" override val icon = R.drawable.mal_logo override val requiresLogin = false - + override val syncIdName = SyncIdName.MyAnimeList + override var requireLibraryRefresh = true override val createAccountUrl = "$mainUrl/register.php" override fun logOut() { + requireLibraryRefresh = true removeAccountKeys() } @@ -90,7 +97,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { fromIntToAnimeStatus(status.status), status.score, status.watchedEpisodes - ) + ).also { + requireLibraryRefresh = requireLibraryRefresh || it + } } data class MalAnime( @@ -248,10 +257,45 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { const val MAL_USER_KEY: String = "mal_user" // user data like profile const val MAL_CACHED_LIST: String = "mal_cached_list" - const val MAL_SHOULD_UPDATE_LIST: String = "mal_should_update_list" const val MAL_UNIXTIME_KEY: String = "mal_unixtime" // When token expires const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api + + fun convertToStatus(string: String): MalStatusType { + return fromIntToAnimeStatus(malStatusAsString.indexOf(string)) + } + + enum class MalStatusType(var value: Int, @StringRes val stringRes: Int) { + Watching(0, R.string.type_watching), + Completed(1, R.string.type_completed), + OnHold(2, R.string.type_on_hold), + Dropped(3, R.string.type_dropped), + PlanToWatch(4, R.string.type_plan_to_watch), + None(-1, R.string.type_none) + } + + private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp } + return when (inp) { + -1 -> MalStatusType.None + 0 -> MalStatusType.Watching + 1 -> MalStatusType.Completed + 2 -> MalStatusType.OnHold + 3 -> MalStatusType.Dropped + 4 -> MalStatusType.PlanToWatch + 5 -> MalStatusType.Watching + else -> MalStatusType.None + } + } + + private fun parseDateLong(string: String?): Long? { + return try { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse( + string ?: return null + )?.time?.div(1000) + } catch (e: Exception) { + null + } + } } override suspend fun handleRedirect(url: String): Boolean { @@ -275,7 +319,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { switchToNewAccount() storeToken(res) val user = getMalUser() - setKey(MAL_SHOULD_UPDATE_LIST, true) + requireLibraryRefresh = true return user != null } } @@ -308,9 +352,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime)) setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token) setKey(accountId, MAL_TOKEN_KEY, token.access_token) + requireLibraryRefresh = true } } catch (e: Exception) { - e.printStackTrace() + logError(e) } } @@ -329,7 +374,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ).text storeToken(res) } catch (e: Exception) { - e.printStackTrace() + logError(e) } } @@ -382,7 +427,24 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class Data( @JsonProperty("node") val node: Node, @JsonProperty("list_status") val list_status: ListStatus?, - ) + ) { + fun toLibraryItem(): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + this.node.title, + "https://myanimelist.net/anime/${this.node.id}/", + this.node.id.toString(), + this.list_status?.num_episodes_watched, + this.node.num_episodes, + this.list_status?.score?.times(10), + parseDateLong(this.list_status?.updated_at), + "MAL", + TvType.Anime, + this.node.main_picture?.large ?: this.node.main_picture?.medium, + null, + null, + ) + } + } data class Paging( @JsonProperty("next") val next: String? @@ -413,18 +475,43 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { return getKey(MAL_CACHED_LIST) as? Array } - suspend fun getMalAnimeListSmart(): Array? { + private suspend fun getMalAnimeListSmart(): Array? { if (getAuth() == null) return null - return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) { + return if (requireLibraryRefresh) { val list = getMalAnimeList() setKey(MAL_CACHED_LIST, list) - setKey(MAL_SHOULD_UPDATE_LIST, false) list } else { getMalAnimeListCached() } } + override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { + val list = getMalAnimeListSmart()?.groupBy { + convertToStatus(it.list_status?.status ?: "").stringRes + }?.mapValues { group -> + group.value.map { it.toLibraryItem() } + } ?: emptyMap() + + // To fill empty lists when MAL does not return them + val baseMap = + MalStatusType.values().filter { it.value >= 0 }.associate { + it.stringRes to emptyList() + } + + return SyncAPI.LibraryMetadata( + (baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) }, + setOf( + ListSorting.AlphabeticalA, + ListSorting.AlphabeticalZ, + ListSorting.UpdatedNew, + ListSorting.UpdatedOld, + ListSorting.RatingHigh, + ListSorting.RatingLow, + ) + ) + } + private suspend fun getMalAnimeList(): Array { checkMalToken() var offset = 0 @@ -440,10 +527,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { return fullList.toTypedArray() } - fun convertToStatus(string: String): MalStatusType { - return fromIntToAnimeStatus(malStatusAsString.indexOf(string)) - } - private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? { val user = "@me" val auth = getAuth() ?: return null @@ -557,28 +640,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { return user } - enum class MalStatusType(var value: Int) { - Watching(0), - Completed(1), - OnHold(2), - Dropped(3), - PlanToWatch(4), - None(-1) - } - - private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp } - return when (inp) { - -1 -> MalStatusType.None - 0 -> MalStatusType.Watching - 1 -> MalStatusType.Completed - 2 -> MalStatusType.OnHold - 3 -> MalStatusType.Dropped - 4 -> MalStatusType.PlanToWatch - 5 -> MalStatusType.Watching - else -> MalStatusType.None - } - } - private suspend fun setScoreRequest( id: Int, status: MalStatusType? = null, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt index 138084fc..b4c07792 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt @@ -7,7 +7,8 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs -class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManager(context, _spanCount) { +class GrdLayoutManager(val context: Context, _spanCount: Int) : + GridLayoutManager(context, _spanCount) { override fun onFocusSearchFailed( focused: View, focusDirection: Int, @@ -34,7 +35,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManage val pos = maxOf(0, getPosition(focused!!) - 2) parent.scrollToPosition(pos) super.onRequestChildFocus(parent, state, child, focused) - } catch (e: Exception){ + } catch (e: Exception) { false } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 8a8f90b4..5cf6fc8e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -569,7 +569,7 @@ class HomeFragment : Fragment() { val mutableListOfResponse = mutableListOf() listHomepageItems.clear() - (home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList( + (home_master_recycler?.adapter as? ParentItemAdapter)?.updateList( d.values.toMutableList(), home_master_recycler ) @@ -621,7 +621,7 @@ class HomeFragment : Fragment() { //home_loaded?.isVisible = false } is Resource.Loading -> { - (home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(listOf()) + (home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(listOf()) home_loading_shimmer?.startShimmer() home_loading?.isVisible = true home_loading_error?.isVisible = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt new file mode 100644 index 00000000..1c6af447 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -0,0 +1,393 @@ +package com.lagradost.cloudstream3.ui.library + +import android.app.Activity +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.AlphaAnimation +import androidx.annotation.StringRes +import androidx.appcompat.widget.SearchView +import androidx.core.view.isVisible +import androidx.fragment.app.activityViewModels +import com.google.android.material.tabs.TabLayoutMediator +import com.lagradost.cloudstream3.APIHolder +import com.lagradost.cloudstream3.APIHolder.allProviders +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.mvvm.Resource +import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD +import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA +import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount +import kotlinx.android.synthetic.main.fragment_library.* +import kotlin.math.abs + +const val LIBRARY_FOLDER = "library_folder" + + +enum class LibraryOpenerType(@StringRes val stringRes: Int) { + Default(R.string.default_subtitles), // TODO FIX AFTER MERGE + Provider(R.string.none), + Browser(R.string.browser), + Search(R.string.search), + None(R.string.none), +} + +/** Used to store how the user wants to open said poster */ +data class LibraryOpener( + val openType: LibraryOpenerType, + val providerData: ProviderLibraryData?, +) + +data class ProviderLibraryData( + val apiName: String +) + +class LibraryFragment : Fragment() { + companion object { + fun newInstance() = LibraryFragment() + + /** + * Store which page was last seen when exiting the fragment and returning + **/ + const val VIEWPAGER_ITEM_KEY = "viewpager_item" + } + + private val libraryViewModel: LibraryViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_library, container, false) + } + + override fun onSaveInstanceState(outState: Bundle) { + viewpager?.currentItem?.let { currentItem -> + outState.putInt(VIEWPAGER_ITEM_KEY, currentItem) + } + super.onSaveInstanceState(outState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + context?.fixPaddingStatusbar(search_status_bar_padding) + + sort_fab?.setOnClickListener { + val methods = libraryViewModel.sortingMethods.map { + txt(it.stringRes).asString(view.context) + } + + activity?.showBottomDialog(methods, + libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), + txt(R.string.sort_by).asString(view.context), + false, + {}, + { + val method = libraryViewModel.sortingMethods[it] + libraryViewModel.sort(method) + }) + } + + main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + libraryViewModel.sort(ListSorting.Query, query) + return true + } + + // This is required to prevent the first text change + // When this is attached it'll immediately send a onQueryTextChange("") + // Which we do not want + var hasInitialized = false + override fun onQueryTextChange(newText: String?): Boolean { + if (!hasInitialized) { + hasInitialized = true + return true + } + + libraryViewModel.sort(ListSorting.Query, newText) + return true + } + }) + + libraryViewModel.reloadPages(false) + + list_selector?.setOnClickListener { + val items = libraryViewModel.availableApiNames + val currentItem = libraryViewModel.currentApiName.value + + activity?.showBottomDialog(items, + items.indexOf(currentItem), + txt(R.string.select_library).asString(it.context), + false, + {}) { index -> + val selectedItem = items.getOrNull(index) ?: return@showBottomDialog + libraryViewModel.switchList(selectedItem) + } + } + + + /** + * Shows a plugin selection dialogue and saves the response + **/ + fun Activity.showPluginSelectionDialog( + key: String, + syncId: SyncIdName, + apiName: String? = null, + ) { + val availableProviders = allProviders.filter { + it.supportedSyncNames.contains(syncId) + }.map { it.name } + + // Add the api if it exists + (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } ?: emptyList()) + + val baseOptions = listOf( + LibraryOpenerType.Default, + LibraryOpenerType.None, + LibraryOpenerType.Browser, + LibraryOpenerType.Search + ) + + val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders + + val savedSelection = getKey(LIBRARY_FOLDER, key) + val selectedIndex = + when { + savedSelection == null -> 0 + // If provider + savedSelection.openType == LibraryOpenerType.Provider + && savedSelection.providerData?.apiName != null -> { + availableProviders.indexOf(savedSelection.providerData.apiName) + .takeIf { it != -1 } + ?.plus(baseOptions.size) ?: 0 + } + // Else base option + else -> baseOptions.indexOf(savedSelection.openType) + } + + this.showBottomDialog( + items, + selectedIndex, + txt(R.string.open_with).asString(this), + false, + {}, + ) { + val savedData = if (it < baseOptions.size) { + LibraryOpener( + baseOptions[it], + null + ) + } else { + LibraryOpener( + LibraryOpenerType.Provider, + ProviderLibraryData(items[it]) + ) + } + + setKey( + LIBRARY_FOLDER, + key, + savedData, + ) + } + } + + provider_selector?.setOnClickListener { + val syncName = libraryViewModel.currentSyncApi?.syncIdName ?: return@setOnClickListener + activity?.showPluginSelectionDialog(syncName.name, syncName) + } + + viewpager?.setPageTransformer(LibraryScrollTransformer()) + viewpager?.adapter = + viewpager.adapter ?: ViewpagerAdapter(mutableListOf(), { isScrollingDown: Boolean -> + if (isScrollingDown) { + sort_fab?.shrink() + } else { + sort_fab?.extend() + } + }) callback@{ searchClickCallback -> + // To prevent future accidents + debugAssert({ + searchClickCallback.card !is SyncAPI.LibraryItem + }, { + "searchClickCallback ${searchClickCallback.card} is not a LibraryItem" + }) + + val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId + val syncName = + libraryViewModel.currentSyncApi?.syncIdName ?: return@callback + + when (searchClickCallback.action) { + SEARCH_ACTION_SHOW_METADATA -> { + activity?.showPluginSelectionDialog( + syncId, + syncName, + searchClickCallback.card.apiName + ) + } + + SEARCH_ACTION_LOAD -> { + // This basically first selects the individual opener and if that is default then + // selects the whole list opener + val savedListSelection = + getKey(LIBRARY_FOLDER, syncName.name) + val savedSelection = getKey(LIBRARY_FOLDER, syncId).takeIf { + it?.openType != LibraryOpenerType.Default + } ?: savedListSelection + + when (savedSelection?.openType) { + null, LibraryOpenerType.Default -> { + // Prevents opening MAL/AniList as a provider + if (APIHolder.getApiFromNameNull(searchClickCallback.card.apiName) != null) { + activity?.loadSearchResult( + searchClickCallback.card + ) + } else { + // Search when no provider can open + QuickSearchFragment.pushSearch( + activity, + searchClickCallback.card.name + ) + } + } + LibraryOpenerType.None -> {} + LibraryOpenerType.Provider -> + savedSelection.providerData?.apiName?.let { apiName -> + activity?.loadResult( + searchClickCallback.card.url, + apiName, + ) + } + LibraryOpenerType.Browser -> + openBrowser(searchClickCallback.card.url) + LibraryOpenerType.Search -> { + QuickSearchFragment.pushSearch( + activity, + searchClickCallback.card.name + ) + } + } + } + } + } + + viewpager?.offscreenPageLimit = 2 + viewpager?.reduceDragSensitivity() + + val startLoading = Runnable { + gridview?.numColumns = context?.getSpanCount() ?: 3 + gridview?.adapter = + context?.let { LoadingPosterAdapter(it, 6 * 3) } + library_loading_overlay?.isVisible = true + library_loading_shimmer?.startShimmer() + empty_list_textview?.isVisible = false + } + + val stopLoading = Runnable { + gridview?.adapter = null + library_loading_overlay?.isVisible = false + library_loading_shimmer?.stopShimmer() + } + + val handler = Handler(Looper.getMainLooper()) + + observe(libraryViewModel.pages) { resource -> + when (resource) { + is Resource.Success -> { + handler.removeCallbacks(startLoading) + val pages = resource.value + val showNotice = pages.all { it.items.isEmpty() } + empty_list_textview?.isVisible = showNotice + if (showNotice) { + if (libraryViewModel.availableApiNames.size > 1) { + empty_list_textview?.setText(R.string.empty_library_logged_in_message) + } else { + empty_list_textview?.setText(R.string.empty_library_no_accounts_message) + } + } + + (viewpager.adapter as? ViewpagerAdapter)?.pages = pages + // Using notifyItemRangeChanged keeps the animations when sorting + viewpager.adapter?.notifyItemRangeChanged(0, viewpager.adapter?.itemCount ?: 0) + + // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating + // Without this there would be a flashing effect: + // loading -> show old viewpager -> black screen -> show new viewpager + handler.postDelayed(stopLoading, 300) + + savedInstanceState?.getInt(VIEWPAGER_ITEM_KEY)?.let { currentPos -> + viewpager?.setCurrentItem(currentPos, false) + savedInstanceState.remove(VIEWPAGER_ITEM_KEY) + } + + // Since the animation to scroll multiple items is so much its better to just hide + // the viewpager a bit while the fastest animation is running + fun hideViewpager(distance: Int) { + if (distance < 3) return + + val hideAnimation = AlphaAnimation(1f, 0f).apply { + duration = distance * 50L + fillAfter = true + } + val showAnimation = AlphaAnimation(0f, 1f).apply { + duration = distance * 50L + startOffset = distance * 100L + fillAfter = true + } + viewpager?.startAnimation(hideAnimation) + viewpager?.startAnimation(showAnimation) + } + + TabLayoutMediator( + library_tab_layout, + viewpager, + ) { tab, position -> + tab.text = pages.getOrNull(position)?.title?.asStringNull(context) + tab.view.setOnClickListener { + val currentItem = viewpager?.currentItem ?: return@setOnClickListener + val distance = abs(position - currentItem) + hideViewpager(distance) + } + }.attach() + } + is Resource.Loading -> { + // Only start loading after 200ms to prevent loading cached lists + handler.postDelayed(startLoading, 200) + } + is Resource.Failure -> { + stopLoading.run() + // No user indication it failed :( + // TODO + } + } + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + (viewpager.adapter as? ViewpagerAdapter)?.rebind() + super.onConfigurationChanged(newConfig) + } +} + +class MenuSearchView(context: Context) : SearchView(context) { + override fun onActionViewCollapsed() { + super.onActionViewCollapsed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt new file mode 100644 index 00000000..8aafbdd6 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt @@ -0,0 +1,17 @@ +package com.lagradost.cloudstream3.ui.library + +import android.view.View +import androidx.viewpager2.widget.ViewPager2 +import kotlinx.android.synthetic.main.library_viewpager_page.view.* +import kotlin.math.roundToInt + +class LibraryScrollTransformer : ViewPager2.PageTransformer { + override fun transformPage(page: View, position: Float) { + val padding = (-position * page.width).roundToInt() + page.page_recyclerview.setPadding( + padding, 0, + -padding, 0 + ) + } +} + diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt new file mode 100644 index 00000000..5f64880c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -0,0 +1,104 @@ +package com.lagradost.cloudstream3.ui.library + +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import kotlinx.coroutines.delay + +enum class ListSorting(@StringRes val stringRes: Int) { + Query(R.string.none), + RatingHigh(R.string.sort_rating_desc), + RatingLow(R.string.sort_rating_asc), + UpdatedNew(R.string.sort_updated_new), + UpdatedOld(R.string.sort_updated_old), + AlphabeticalA(R.string.sort_alphabetical_a), + AlphabeticalZ(R.string.sort_alphabetical_z), +} + +const val LAST_SYNC_API_KEY = "last_sync_api" + +class LibraryViewModel : ViewModel() { + private val _pages: MutableLiveData>> = MutableLiveData(null) + val pages: LiveData>> = _pages + + private val _currentApiName: MutableLiveData = MutableLiveData("") + val currentApiName: LiveData = _currentApiName + + private val availableSyncApis + get() = SyncApis.filter { it.hasAccount() } + + var currentSyncApi = availableSyncApis.let { allApis -> + val lastSelection = getKey(LAST_SYNC_API_KEY) + availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull() + } + private set(value) { + field = value + setKey(LAST_SYNC_API_KEY, field?.name) + } + + val availableApiNames: List + get() = availableSyncApis.map { it.name } + + var sortingMethods = emptyList() + private set + + var currentSortingMethod: ListSorting? = sortingMethods.firstOrNull() + private set + + fun switchList(name: String) { + currentSyncApi = availableSyncApis[availableApiNames.indexOf(name)] + _currentApiName.postValue(currentSyncApi?.name) + reloadPages(true) + } + + fun sort(method: ListSorting, query: String? = null) { + val currentList = pages.value ?: return + currentSortingMethod = method + (currentList as? Resource.Success)?.value?.forEachIndexed { _, page -> + page.sort(method, query) + } + _pages.postValue(currentList) + } + + fun reloadPages(forceReload: Boolean) { + // Only skip loading if its not forced and pages is not empty + if (!forceReload && (pages.value as? Resource.Success)?.value?.isNotEmpty() == true && + currentSyncApi?.requireLibraryRefresh != true + ) return + + ioSafe { + currentSyncApi?.let { repo -> + _currentApiName.postValue(repo.name) + _pages.postValue(Resource.Loading()) + val libraryResource = repo.getPersonalLibrary() + if (libraryResource is Resource.Failure) { + _pages.postValue(libraryResource) + return@let + } + val library = (libraryResource as? Resource.Success)?.value ?: return@let + + sortingMethods = library.supportedListSorting.toList() + currentSortingMethod = null + + repo.requireLibraryRefresh = false + + val pages = library.allLibraryLists.map { + SyncAPI.Page( + it.name, + it.items + ) + } + + _pages.postValue(Resource.Success(pages)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt new file mode 100644 index 00000000..a637133b --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt @@ -0,0 +1,37 @@ +package com.lagradost.cloudstream3.ui.library + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.ListPopupWindow.MATCH_PARENT +import android.widget.RelativeLayout +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.UIHelper.toPx +import kotlinx.android.synthetic.main.loading_poster_dynamic.view.* +import kotlin.math.roundToInt +import kotlin.math.sqrt + +class LoadingPosterAdapter(context: Context, private val itemCount: Int) : + BaseAdapter() { + private val inflater: LayoutInflater = LayoutInflater.from(context) + + override fun getCount(): Int { + return itemCount + } + + override fun getItem(position: Int): Any? { + return null + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + return convertView ?: inflater.inflate(R.layout.loading_poster_dynamic, parent, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt new file mode 100644 index 00000000..2435f8be --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt @@ -0,0 +1,130 @@ +package com.lagradost.cloudstream3.ui.library + +import android.content.res.ColorStateList +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.AcraApplication +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.ui.AutofitRecyclerView +import com.lagradost.cloudstream3.ui.search.SearchClickCallback +import com.lagradost.cloudstream3.ui.search.SearchResultBuilder +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.UIHelper.toPx +import kotlinx.android.synthetic.main.search_result_grid_expanded.view.* +import kotlin.math.roundToInt + + +class PageAdapter( + override val items: MutableList, + private val resView: AutofitRecyclerView, + val clickCallback: (SearchClickCallback) -> Unit +) : + AppUtils.DiffAdapter(items) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return LibraryItemViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.search_result_grid_expanded, parent, false) + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is LibraryItemViewHolder -> { + holder.bind(items[position], position) + } + } + } + + private fun isDark(color: Int): Boolean { + return ColorUtils.calculateLuminance(color) < 0.5 + } + + fun getDifferentColor(color: Int, ratio: Float = 0.7f): Int { + return if (isDark(color)) { + ColorUtils.blendARGB(color, Color.WHITE, ratio) + } else { + ColorUtils.blendARGB(color, Color.BLACK, ratio) + } + } + + inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val cardView: ImageView = itemView.imageView + + private val compactView = false//itemView.context.getGridIsCompact() + private val coverHeight: Int = + if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt() + + fun bind(item: SyncAPI.LibraryItem, position: Int) { + /** https://stackoverflow.com/questions/8817522/how-to-get-color-code-of-image-view */ + + SearchResultBuilder.bind( + this@PageAdapter.clickCallback, + item, + position, + itemView, + colorCallback = { palette -> + AcraApplication.context?.let { ctx -> + val defColor = ContextCompat.getColor(ctx, R.color.ratingColorBg) + var bg = palette.getDarkVibrantColor(defColor) + if (bg == defColor) { + bg = palette.getDarkMutedColor(defColor) + } + if (bg == defColor) { + bg = palette.getVibrantColor(defColor) + } + + val fg = + getDifferentColor(bg)//palette.getVibrantColor(ContextCompat.getColor(ctx,R.color.ratingColor)) + itemView.text_rating.apply { + setTextColor(ColorStateList.valueOf(fg)) + } + itemView.text_rating_holder?.backgroundTintList = ColorStateList.valueOf(bg) + itemView.watchProgress?.apply { + progressTintList = ColorStateList.valueOf(fg) + progressBackgroundTintList = ColorStateList.valueOf(bg) + } + } + } + ) + + // See searchAdaptor for this, it basically fixes the height + if (!compactView) { + cardView.apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + coverHeight + ) + } + } + + val showProgress = item.episodesCompleted != null && item.episodesTotal != null + itemView.watchProgress.isVisible = showProgress + if (showProgress) { + itemView.watchProgress.max = item.episodesTotal!! + itemView.watchProgress.progress = item.episodesCompleted!! + } + + itemView.imageText.text = item.name + + val showRating = (item.personalRating ?: 0) != 0 + itemView.text_rating_holder.isVisible = showRating + if (showRating) { + // We want to show 8.5 but not 8.0 hence the replace + val rating = ((item.personalRating ?: 0).toDouble() / 10).toString() + .replace(".0", "") + + itemView.text_rating.text = "★ $rating" + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt new file mode 100644 index 00000000..33a40386 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -0,0 +1,90 @@ +package com.lagradost.cloudstream3.ui.library + +import android.os.Build +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.doOnAttach +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.OnFlingListener +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.ui.search.SearchClickCallback +import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount +import kotlinx.android.synthetic.main.library_viewpager_page.view.* + +class ViewpagerAdapter( + var pages: List, + val scrollCallback: (isScrollingDown: Boolean) -> Unit, + val clickCallback: (SearchClickCallback) -> Unit +) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return PageViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.library_viewpager_page, parent, false) + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is PageViewHolder -> { + holder.bind(pages[position], unbound.remove(position)) + } + } + } + + private val unbound = mutableSetOf() + /** + * Used to mark all pages for re-binding and forces all items to be refreshed + * Without this the pages will still use the same adapters + **/ + fun rebind() { + unbound.addAll(0..pages.size) + this.notifyItemRangeChanged(0, pages.size) + } + + inner class PageViewHolder(private val itemViewTest: View) : + RecyclerView.ViewHolder(itemViewTest) { + fun bind(page: SyncAPI.Page, rebind: Boolean) { + itemView.page_recyclerview?.spanCount = + this@PageViewHolder.itemView.context.getSpanCount() ?: 3 + + if (itemViewTest.page_recyclerview?.adapter == null || rebind) { + // Only add the items after it has been attached since the items rely on ItemWidth + // Which is only determined after the recyclerview is attached. + // If this fails then item height becomes 0 when there is only one item + itemViewTest.page_recyclerview?.doOnAttach { + itemViewTest.page_recyclerview?.adapter = PageAdapter( + page.items.toMutableList(), + itemViewTest.page_recyclerview, + clickCallback + ) + } + } else { + (itemViewTest.page_recyclerview?.adapter as? PageAdapter)?.updateList(page.items) + itemViewTest.page_recyclerview?.scrollToPosition(0) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + itemViewTest.page_recyclerview.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY -> + val diff = scrollY - oldScrollY + if (diff == 0) return@setOnScrollChangeListener + + scrollCallback.invoke(diff > 0) + } + } else { + itemViewTest.page_recyclerview.onFlingListener = object : OnFlingListener() { + override fun onFling(velocityX: Int, velocityY: Int): Boolean { + scrollCallback.invoke(velocityY > 0) + return false + } + } + } + + } + } + + override fun getItemCount(): Int { + return pages.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index f29b6921..305da69c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -607,7 +607,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player_top_holder?.isGone = isGone //player_episodes_button?.isVisible = !isGone && hasEpisodes player_video_title?.isGone = togglePlayerTitleGone - player_video_title_rez?.isGone = isGone +// player_video_title_rez?.isGone = isGone player_episode_filler?.isGone = isGone player_center_menu?.isGone = isGone player_lock?.isGone = !isShowing diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index bf39edc7..67f58195 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -11,9 +11,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.inputmethod.EditorInfo import android.widget.* -import android.widget.TextView.OnEditorActionListener import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.animation.addListener @@ -528,7 +526,7 @@ class GeneratorPlayer : FullScreenPlayer() { } } - var selectSourceDialog: AlertDialog? = null + var selectSourceDialog: Dialog? = null // var selectTracksDialog: AlertDialog? = null override fun showMirrorsDialogue() { @@ -540,10 +538,8 @@ class GeneratorPlayer : FullScreenPlayer() { player.handleEvent(CSPlayerEvent.Pause) val currentSubtitles = sortSubs(currentSubs) - val sourceBuilder = AlertDialog.Builder(ctx, R.style.AlertDialogCustomBlack) - .setView(R.layout.player_select_source_and_subs) - - val sourceDialog = sourceBuilder.create() + val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) + sourceDialog.setContentView(R.layout.player_select_source_and_subs) selectSourceDialog = sourceDialog @@ -1149,13 +1145,15 @@ class GeneratorPlayer : FullScreenPlayer() { val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name ?: "NULL" - player_video_title_rez?.text = when (titleRez) { + val title = when (titleRez) { 0 -> "" 1 -> extra 2 -> source 3 -> "$source - $extra" else -> "" } + player_video_title_rez?.text = title + player_video_title_rez?.isVisible = title.isNotBlank() } override fun playerDimensionsLoaded(widthHeight: Pair) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index ad3d9eb8..ba57d2de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -220,7 +220,7 @@ class QuickSearchFragment : Fragment() { when (it) { is Resource.Success -> { it.value.let { data -> - (quick_search_autofit_results?.adapter as? SearchAdapter?)?.updateList( + (quick_search_autofit_results?.adapter as? SearchAdapter)?.updateList( context?.filterSearchResultByFilmQuality(data) ?: data ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 9cfbf45c..2e2e46b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -277,7 +277,7 @@ open class ResultFragment : ResultTrailerPlayer() { private var downloadButton: EasyDownloadButton? = null override fun onDestroyView() { updateUIListener = null - (result_episodes?.adapter as EpisodeAdapter?)?.killAdapter() + (result_episodes?.adapter as? EpisodeAdapter)?.killAdapter() downloadButton?.dispose() super.onDestroyView() @@ -458,7 +458,7 @@ open class ResultFragment : ResultTrailerPlayer() { temporary_no_focus?.requestFocus() } - (result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value) + (result_episodes?.adapter as? EpisodeAdapter)?.updateList(episodes.value) if (isTv && hasEpisodes) main { delay(500) @@ -687,7 +687,7 @@ open class ResultFragment : ResultTrailerPlayer() { val newList = list.filter { it.isSynced && it.hasAccount } result_mini_sync?.isVisible = newList.isNotEmpty() - (result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon }) + (result_mini_sync?.adapter as? ImageAdapter)?.updateList(newList.mapNotNull { it.icon }) } var currentSyncProgress = 0 @@ -900,7 +900,7 @@ open class ResultFragment : ResultTrailerPlayer() { result_cast_items?.isVisible = d.actors != null - (result_cast_items?.adapter as ActorAdaptor?)?.apply { + (result_cast_items?.adapter as? ActorAdaptor)?.apply { updateList(d.actors ?: emptyList()) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 9bae8753..b38e1765 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -485,7 +485,7 @@ class ResultFragmentPhone : ResultFragment() { result_recommendations?.post { rec?.let { list -> - (result_recommendations?.adapter as SearchAdapter?)?.updateList(list.filter { it.apiName == matchAgainst }) + (result_recommendations?.adapter as? SearchAdapter)?.updateList(list.filter { it.apiName == matchAgainst }) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index d5cab1a6..2bd8ff0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -107,7 +107,7 @@ class ResultFragmentTv : ResultFragment() { result_recommendations?.isGone = isInvalid result_recommendations_holder?.isGone = isInvalid val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName - (result_recommendations?.adapter as SearchAdapter?)?.updateList(rec?.filter { it.apiName == matchAgainst } + (result_recommendations?.adapter as? SearchAdapter)?.updateList(rec?.filter { it.apiName == matchAgainst } ?: emptyList()) rec?.map { it.apiName }?.distinct()?.let { apiNames -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 6ed32b15..6817af6a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -13,6 +13,7 @@ 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.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -1443,12 +1444,18 @@ class ResultViewModel2 : ViewModel() { val realRecommendations = ArrayList() // TODO: fix - //val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name) - // meta.recommendations?.forEach { rec -> - // apiNames.forEach { name -> - // realRecommendations.add(rec.copy(apiName = name)) - // } - // } + val apiNames = apis.filter { + it.name.contains("gogoanime", true) || + it.name.contains("9anime", true) + }.map { + it.name + } + + meta.recommendations?.forEach { rec -> + apiNames.forEach { name -> + realRecommendations.add(rec.copy(apiName = name)) + } + } recommendations = recommendations?.union(realRecommendations)?.toList() ?: realRecommendations @@ -2143,7 +2150,7 @@ class ResultViewModel2 : ViewModel() { val validUrlResource = safeApiCall { SyncRedirector.redirect( url, - api.mainUrl + api ) } // TODO: fix diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt index ddf559fc..649641c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt @@ -10,12 +10,16 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.ui.AutofitRecyclerView +import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx import kotlinx.android.synthetic.main.search_result_compact.view.* import kotlin.math.roundToInt +/** Click */ const val SEARCH_ACTION_LOAD = 0 + +/** Long press */ const val SEARCH_ACTION_SHOW_METADATA = 1 const val SEARCH_ACTION_PLAY_FILE = 2 const val SEARCH_ACTION_FOCUSED = 4 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 4144a042..b4a38216 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -420,7 +420,7 @@ class SearchFragment : Fragment() { is Resource.Success -> { it.value.let { data -> if (data.isNotEmpty()) { - (search_autofit_results?.adapter as SearchAdapter?)?.updateList(data) + (search_autofit_results?.adapter as? SearchAdapter)?.updateList(data) } } searchExitIcon.alpha = 1f diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index 3afbb8c0..3447ee32 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -1,12 +1,14 @@ package com.lagradost.cloudstream3.ui.search import android.content.Context +import android.graphics.drawable.Drawable import android.view.View import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.view.isVisible +import androidx.palette.graphics.Palette import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings @@ -41,6 +43,7 @@ object SearchResultBuilder { nextFocusBehavior: Boolean? = null, nextFocusUp: Int? = null, nextFocusDown: Int? = null, + colorCallback : ((Palette) -> Unit)? = null ) { val cardView: ImageView = itemView.imageView val cardText: TextView? = itemView.imageText @@ -100,7 +103,7 @@ object SearchResultBuilder { cardText?.isVisible = showTitle cardView.isVisible = true - if (!cardView.setImage(card.posterUrl, card.posterHeaders)) { + if (!cardView.setImage(card.posterUrl, card.posterHeaders, colorCallback = colorCallback)) { cardView.setImageResource(R.drawable.default_cover) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 1dcaf350..3f1c781a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -85,6 +85,7 @@ val appLanguages = arrayListOf( Triple("\uD83C\uDDF5\uD83C\uDDF9", "Portuguese", "pt"), Triple("", "Romanian", "ro"), Triple("", "Russian", "ru"), + Triple("", "Slovak", "sk"), Triple("", "Somali", "so"), Triple("", "Swedish", "sv"), Triple("", "Tamil", "ta"), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index bd44a058..d328d226 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -143,7 +143,7 @@ class PluginsFragment : Fragment() { } observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) -> - (plugin_recycler_view?.adapter as? PluginAdapter?)?.updateList(list) + (plugin_recycler_view?.adapter as? PluginAdapter)?.updateList(list) if (scrollToTop) plugin_recycler_view?.scrollToPosition(0) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index d563bffa..00dee9b2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -28,10 +28,12 @@ import androidx.core.text.toSpanned import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.tvprovider.media.tv.* import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor +import androidx.viewpager2.widget.ViewPager2 import com.fasterxml.jackson.module.kotlin.readValue import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState @@ -65,6 +67,7 @@ import okhttp3.Cache import java.io.* import java.net.URL import java.net.URLDecoder +import kotlin.system.measureTimeMillis object AppUtils { fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) { @@ -164,6 +167,18 @@ object AppUtils { return builder.build() } + // https://stackoverflow.com/a/67441735/13746422 + fun ViewPager2.reduceDragSensitivity(f: Int = 4) { + val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView") + recyclerViewField.isAccessible = true + val recyclerView = recyclerViewField.get(this) as RecyclerView + + val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop") + touchSlopField.isAccessible = true + val touchSlop = touchSlopField.get(recyclerView) as Int + touchSlopField.set(recyclerView, touchSlop * f) // "8" was obtained experimentally + } + @SuppressLint("RestrictedApi") fun getAllWatchNextPrograms(context: Context): Set { val COLUMN_WATCH_NEXT_ID_INDEX = 0 @@ -329,6 +344,46 @@ object AppUtils { } } + abstract class DiffAdapter( + open val items: MutableList, + val comparison: (first: T, second: T) -> Boolean = { first, second -> + first.hashCode() == second.hashCode() + } + ) : + RecyclerView.Adapter() { + override fun getItemCount(): Int { + return items.size + } + + fun updateList(newList: List) { + val diffResult = DiffUtil.calculateDiff( + GenericDiffCallback(this.items, newList) + ) + + items.clear() + items.addAll(newList) + + diffResult.dispatchUpdatesTo(this) + } + + inner class GenericDiffCallback( + private val oldList: List, + private val newList: List + ) : + DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + comparison(oldList[oldItemPosition], newList[newItemPosition]) + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition] == newList[newItemPosition] + } + } + + fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) { runOnUiThread { val context = this diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 80e5d64a..8d51e5ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -18,13 +18,11 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.PLUGINS_KEY import com.lagradost.cloudstream3.plugins.PLUGINS_KEY_LOCAL import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST -import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_SHOULD_UPDATE_LIST import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_TOKEN_KEY import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_USER_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_REFRESH_TOKEN_KEY -import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_SHOULD_UPDATE_LIST import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_TOKEN_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY @@ -52,12 +50,10 @@ object BackupUtils { // When sharing backup we do not want to transfer what is essentially the password ANILIST_TOKEN_KEY, ANILIST_CACHED_LIST, - ANILIST_SHOULD_UPDATE_LIST, ANILIST_UNIXTIME_KEY, ANILIST_USER_KEY, MAL_TOKEN_KEY, MAL_REFRESH_TOKEN_KEY, - MAL_SHOULD_UPDATE_LIST, MAL_CACHED_LIST, MAL_UNIXTIME_KEY, MAL_USER_KEY, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 9174c481..281c9c44 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder.capitalize import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -10,6 +11,8 @@ import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.syncproviders.AccountManager +import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.result.VideoWatchState @@ -51,7 +54,20 @@ object DataStoreHelper { @JsonProperty("year") val year: Int?, @JsonProperty("quality") override var quality: SearchQuality? = null, @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, - ) : SearchResponse + ) : SearchResponse { + fun toLibraryItem(id: String): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + name, + url, + id, + null, + null, + null, + null, + apiName, type, posterUrl, posterHeaders, quality, this.id + ) + } + } data class ResumeWatchingResult( @JsonProperty("name") override val name: String, @@ -71,6 +87,9 @@ object DataStoreHelper { @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, ) : SearchResponse + /** + * A datastore wide account for future implementations of a multiple account system + **/ private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION fun getAllWatchStateIds(): List? { @@ -177,6 +196,7 @@ object DataStoreHelper { fun setBookmarkedData(id: Int?, data: BookmarkedData) { if (id == null) return setKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString(), data) + AccountManager.localListApi.requireLibraryRefresh = true } fun getBookmarkedData(id: Int?): BookmarkedData? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index bd4f8705..1ad3639b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -291,6 +291,7 @@ val extractorApis: MutableList = arrayListOf( Supervideo(), GuardareStream(), CineGrabber(), + Vanfem(), // StreamSB.kt works // SBPlay(), @@ -321,6 +322,7 @@ val extractorApis: MutableList = arrayListOf( DesuDrive(), Filesim(), + FileMoon(), Linkbox(), Acefile(), SpeedoStream(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 7dda3e18..e5f2f2dc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -4,6 +4,7 @@ package com.lagradost.cloudstream3.utils import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder.apis //import com.lagradost.cloudstream3.animeproviders.AniflixProvider import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError @@ -78,17 +79,21 @@ object SyncUtil { return null } - suspend fun getUrlsFromId(id: String, type: String = "anilist") : List { - return arrayListOf() - // val url = - // "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json" - // val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed() - // val pages = response.pages ?: return emptyList() - // val current = pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values).mapNotNull { it.url }.toMutableList() - // if(type == "anilist") { // TODO MAKE BETTER - // current.add("${AniflixProvider().mainUrl}/anime/$id") - // } - // return current + suspend fun getUrlsFromId(id: String, type: String = "anilist"): List { + val url = + "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json" + val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed() + val pages = response.pages ?: return emptyList() + val current = + pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values) + .mapNotNull { it.url }.toMutableList() + + if (type == "anilist") { // TODO MAKE BETTER + apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { + current.add("${it.mainUrl}/anime/$id") + } + } + return current } data class SyncPage( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index 63b3623d..c300d615 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -9,7 +9,9 @@ import android.content.Context import android.content.pm.PackageManager import android.content.res.Configuration import android.content.res.Resources +import android.graphics.Bitmap import android.graphics.Color +import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.view.* @@ -28,15 +30,21 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.graphics.alpha import androidx.core.graphics.blue +import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.graphics.green import androidx.core.graphics.red import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment +import androidx.palette.graphics.Palette import androidx.preference.PreferenceManager +import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings @@ -105,7 +113,7 @@ object UIHelper { listView.requestLayout() } - fun Activity?.getSpanCount(): Int? { + fun Context?.getSpanCount(): Int? { val compactView = false val spanCountLandscape = if (compactView) 2 else 6 val spanCountPortrait = if (compactView) 1 else 3 @@ -158,12 +166,27 @@ object UIHelper { return color } + var createPaletteAsyncCache: HashMap = hashMapOf() + fun createPaletteAsync(url: String, bitmap: Bitmap, callback: (Palette) -> Unit) { + createPaletteAsyncCache[url]?.let { palette -> + callback.invoke(palette) + return + } + Palette.from(bitmap).generate { paletteNull -> + paletteNull?.let { palette -> + createPaletteAsyncCache[url] = palette + callback(palette) + } + } + } + fun ImageView?.setImage( url: String?, headers: Map? = null, @DrawableRes errorImageDrawable: Int? = null, - fadeIn: Boolean = true + fadeIn: Boolean = true, + colorCallback: ((Palette) -> Unit)? = null ): Boolean { if (this == null || url.isNullOrBlank()) return false @@ -177,6 +200,33 @@ object UIHelper { else req } + if (colorCallback != null) { + builder.listener(object : RequestListener { + @SuppressLint("CheckResult") + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + resource?.toBitmapOrNull() + ?.let { bitmap -> createPaletteAsync(url, bitmap, colorCallback) } + return false + } + + @SuppressLint("CheckResult") + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + return false + } + }) + } + val res = if (errorImageDrawable != null) builder.error(errorImageDrawable).into(this) else diff --git a/app/src/main/res/color/item_select_color.xml b/app/src/main/res/color/item_select_color.xml index 0d2834dd..3d69c540 100644 --- a/app/src/main/res/color/item_select_color.xml +++ b/app/src/main/res/color/item_select_color.xml @@ -1,5 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_collections_bookmark_24.xml b/app/src/main/res/drawable/ic_baseline_collections_bookmark_24.xml new file mode 100644 index 00000000..fc90e300 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_collections_bookmark_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_sort_24.xml b/app/src/main/res/drawable/ic_baseline_sort_24.xml new file mode 100644 index 00000000..96d46231 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_sort_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_star_24.xml b/app/src/main/res/drawable/ic_baseline_star_24.xml index ab099425..2dcadb7f 100644 --- a/app/src/main/res/drawable/ic_baseline_star_24.xml +++ b/app/src/main/res/drawable/ic_baseline_star_24.xml @@ -1,5 +1,5 @@ - + android:width="12dp" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/app/src/main/res/drawable/ic_outline_account_circle_24.xml b/app/src/main/res/drawable/ic_outline_account_circle_24.xml new file mode 100644 index 00000000..cc564471 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_account_circle_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/indicator_background.xml b/app/src/main/res/drawable/indicator_background.xml new file mode 100644 index 00000000..ef44fb7c --- /dev/null +++ b/app/src/main/res/drawable/indicator_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/rating_bg_color.xml b/app/src/main/res/drawable/rating_bg_color.xml new file mode 100644 index 00000000..60e62bab --- /dev/null +++ b/app/src/main/res/drawable/rating_bg_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ad29d22a..b6290865 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -35,9 +35,9 @@ --> @@ -132,7 +131,8 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 8b9b9e36..afbf735d 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -129,9 +129,9 @@ + android:paddingBottom="100dp" + android:clipToPadding="false" + android:layout_height="wrap_content"> + + + + diff --git a/app/src/main/res/layout/loading_poster_dynamic.xml b/app/src/main/res/layout/loading_poster_dynamic.xml new file mode 100644 index 00000000..11855acb --- /dev/null +++ b/app/src/main/res/layout/loading_poster_dynamic.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 691795d3..683a1077 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -96,33 +96,36 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + app:layout_constraintTop_toTopOf="parent"> - + + + + @@ -319,23 +322,23 @@ + tools:text="Skip Opening" + tools:visibility="visible" /> @@ -117,6 +118,7 @@ android:nextFocusLeft="@id/sort_providers" android:nextFocusRight="@id/cancel_btt" android:requiresFadingEdge="vertical" + tools:layout_height="200dp" tools:listfooter="@layout/sort_bottom_footer_add_choice" tools:listitem="@layout/sort_bottom_single_choice" /> diff --git a/app/src/main/res/layout/search_result_grid_expanded.xml b/app/src/main/res/layout/search_result_grid_expanded.xml index 25b3f67d..47fd7cd3 100644 --- a/app/src/main/res/layout/search_result_grid_expanded.xml +++ b/app/src/main/res/layout/search_result_grid_expanded.xml @@ -5,62 +5,109 @@ android:id="@+id/search_result_root" android:layout_width="match_parent" android:layout_height="wrap_content" - android:clickable="true" android:focusable="true" android:foreground="@drawable/outline_drawable" android:orientation="vertical"> - + android:layout_height="wrap_content"> - - android:layout_height="match_parent" - android:contentDescription="@string/search_poster_img_des" - android:duplicateParentState="true" - android:foreground="?android:attr/selectableItemBackgroundBorderless" - android:scaleType="centerCrop" - tools:src="@drawable/example_poster" /> - - - - + + android:id="@+id/text_quality" + style="@style/TypeButton" /> - - - + + + + + + + + + + + + + + + + - - + + - \ No newline at end of file + diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index 3a5e0929..cb620bb8 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -1,20 +1,23 @@ - + android:id="@+id/navigation_home" + android:icon="@drawable/home_alt" + android:title="@string/title_home" /> + android:id="@+id/navigation_search" + android:icon="@drawable/search_icon" + android:title="@string/title_search" /> + android:id="@+id/navigation_library" + android:icon="@drawable/ic_outline_account_circle_24" + android:title="@string/library" /> + android:id="@+id/navigation_downloads" + android:icon="@drawable/netflix_download" + android:title="@string/title_downloads" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/library_menu.xml b/app/src/main/res/menu/library_menu.xml new file mode 100644 index 00000000..f21d998d --- /dev/null +++ b/app/src/main/res/menu/library_menu.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 14d750a0..d71eeb06 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -144,6 +144,15 @@ app:popEnterAnim="@anim/enter_anim" app:popExitAnim="@anim/exit_anim" /> + + بدأ التحديث تم تنزيل الإضافة إزالة من المشاهدة + الترتيب الأبجدي (من الألف إلى الياء) + اختر المكتبة + المتصفح + محدث (من الأحدث إلى الأقدم) + يبدو أن هذه القائمة فارغة ، حاول التبديل إلى قائمة أخرى + التقييم (من الأعلى إلى الأدنى) + التقييم (من الأدنى إلى الأعلى) + الترتيب الأبجدي (من ي إلى أ) + يبدو أن مكتبتك فارغة :( +\nتسجيل الدخول إلى حساب مكتبة أو إضافة عروض إلى مكتبتك المحلية + محدث (من القديم إلى الجديد) + فرز حسب + افرز + فتح بواسطة + المكتبة + تم العثور على ملف الوضع الآمن! +\nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف. \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 324f44bc..9f95eb3f 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -497,4 +497,5 @@ Приставката е изтеглена Приложението ще се актуализира при изход от него Започна Актуализация + Премахване от гледани \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 33afd571..63ed5444 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -17,7 +17,7 @@ Intro Verlauf löschen Verlauf - Überspringen Button für Openings/Endings anzeigen + Überspringen Knopf für Openings/Endings anzeigen Zu viel Text. Kann nicht in der Zwischenablage gespeichert werden. Episodenvorschaubild Medienvorschaubild @@ -489,4 +489,21 @@ Die Anwendung wird beim Beenden aktualisiert Das Plugin wurde heruntergeladen Von geschaut entfernen + Bibliothek + Browser + Sortieren nach + Sortieren + Bewertung (gut bis schlecht) + Bewertung (schlecht bis gut) + Aktualisiert (neu bis alt) + Aktualisiert (alt bis neu) + Alphabetisch (A bis Z) + Alphabetisch (Z bis A) + Bibliothek auswählen + Öffnen mit + Sieht aus, als wäre deine Bibliothek leer :( +\nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu + Diese Liste scheint leer zu sein. Versuche, zu einer anderen Liste zu wechseln. + Datei für abgesicherten Modus gefunden! +\nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index dc7088cc..0d0b7fb2 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -314,7 +314,7 @@ Αναφορά κατάρρευσης Τι θα θέλατε να δείτε Έγινε - Πρόσθετα + Extensions Προσθήκη αποθετηρίου Όνομα αποθετηρίου Σύνδεσμος αποθετηρίου @@ -490,4 +490,22 @@ Το πρόσθετο κατέβει Ενημέρωση ξεκίνησε Η εφαρμογή θα ενημερωθεί κατά την έξοδο + Αλφαβητικά (Ω προς Α) + Ταξινόμηση + Κριτική (Χαμηλή προς Υψηλή) + Ενημερωμένο (Καινούριο προς παλιό) + Ενημερωμένο (Παλιό προς Καινούργιο) + Βιβλιοθήκη + Κριτική (Υψηλή προς χαμηλή) + Ταξινόμηση με βάση + Αλφαβητικά (Α προς Ω) + Διάλεξε βιβλιοθήκη + Φαίνεται πως η λίστα είναι άδεια, δοκίμασε να μεταβείς σε μία άλλη + Αφαίρεση από παρακολουθημένα + Περιηγητής + Άνοιγμα με + Φαίνεται πως η βιβλιοθήκη σου είναι άδεια :( +\nΣυνδέσου σε έναν λογαριασμό που έχει βιβλιοθήκη, ή πρόσθεσε σειρές στην τοπική βιβλιοθήκη σου + Βρέθηκε αρχείο Ασφαλούς Λειτουργίας! +\nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο. \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d12ae5b0..00dafb16 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -489,6 +489,23 @@ Actualización iniciada Complemento descargado Quitar de visto + Ordenar por + Ordenar + Valoración (más a menos) + Valoración (menos a más) + Actualizado (nuevo a viejo) + Actualizado (viejo a nuevo) + Alfabéticamente (A a Z) + Navegador + Biblioteca + Parece que esta lista está vacía, intenta cambiar a otra + Alfabéticamente (Z a A) + Seleccionar biblioteca + Abrir con + Parece que tu biblioteca está vacía :( +\nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local + ¡Se encontró un archivo en modo seguro! +\nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. Jugadora mostrada - buscar cantidad Jugadora oculta - buscar cantidad Android TV diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 83dc6ee9..96c5950b 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -453,7 +453,7 @@ Semua fitur tambahkan dimatikan karena crash, untuk memudahkanmu mencari penyebab crash. Kode bahasa (en) Ambil dari internet - Putar vidio di bahasa ini + Putar video di bahasa ini Tambah Repositori Pilih ini untuk menghapus semua repositori plugin Lewati pengaturan @@ -483,7 +483,7 @@ Gerakan Beberapa perangkat tidak mendukung penginstal paket mode baru. Coba mode lama jika pembaruan tidak dapat diinstal. Aksi - Referensi + Referer Ya Pasang dulu fitur tambahan Semua Bahasa @@ -512,4 +512,21 @@ Aplikasi akan diperbaharui pada saat keluar Pembaharuan Dimulai Hapus dari tontonan + Browser + Pilih pustaka + Yahh daftar pustaka kamu kosong :( +\nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu + Pustaka + Urutkan berdasar + Urutkan + Peringkat (Rendah ke Tinggi) + Update (Lama ke Terbaru) + Peringkat (Tinggi ke Rendah) + Update (Terbaru ke Lama) + Abjad (A ke Z) + Abjad (Z ke A) + Buka dengan + Yahh daftar ini kosong, coba ganti ke yang lain + Mode aman file ditemukan! +\nTidak memuat ekstensi pada startup sampai berkas dihapus. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b4ba292e..419818a2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -511,4 +511,21 @@ Aggiornamento avviato Plugin scaricato Rimuovi dai già visti + Browser + Ordina per + Punteggio (Decrescente) + Punteggio (Crescente) + Aggiornato (Da nuovo a vecchio) + Aggiornato (Da vecchio a nuovo) + Alfabetico (A - Z) + Alfabetico (Z - A) + Sembra che la tua libreria sia vuota :( +\nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale + Seleziona libreria + Apri con + Libreria + Ordina + Sembra che questa lista sia vuota, prova a passare a un\'altra + File \"safe mode\" trovato! +\nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso. \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 244ae2e1..e4b74300 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -492,4 +492,21 @@ Rozpoczęto aktualizację Pobrano rozszerzenie Usuń z obejrzanych + Przeglądarka + Data aktualizacji (od nowego do starego) + Sortuj według + Sortuj + Otwórz za pomocą + Ocena (od najwyższej do najniższej) + Ocena (od najniższej do najwyższej) + Data aktualizacji (od starego do nowego) + Alfabetycznie (od A do Z) + Alfabetycznie (od Z do A) + Wybierz bibliotekę + Biblioteka + Wygląda na to, że twoja biblioteka jest pusta :( +\nZaloguj się na swoje konto lub dodaj programy do swojej lokalnej biblioteki + Wygląda na to, że ta lista jest pusta, spróbuj przełączyć się na inną + Znaleziono plik trybu bezpiecznego. +\nRozszerzenia nie zostaną wczytane, dopóki plik nie zostanie usunięty. \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 28673fe2..982546bc 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -141,7 +141,7 @@ Copie de rezervă a datelor Fișier de rezervă încărcat Imposibilitatea de a restaura datele din %s - Datele au fost salvate cu succes + Date stocate Permisiuni de arhivare lipsă, vă rugăm să încercați din nou Eroare de backup %s Căutare @@ -380,4 +380,8 @@ CloudStream Vizionează trailerul Actualizarea a început + Actualizați progresul ceasului + Începe următorul episod când se termină episodul curent + Ascundeți calitatea video selectată în rezultatele căutării + Redare Livestream \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 930437c7..537bdb7d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -12,7 +12,7 @@ Скачать неудачный Подогнать Удалить - Всё + Все Пауза Актёрский состав: %s Название источника @@ -25,12 +25,12 @@ Серия %d будет выпущен в Плакат \@нить/результат_плокат_картинка_ - Серия плакат - Главный плакат + Постер Эпизода + Главный постер Следующий случайный Вернуться Изменить поставщика - Фон предпросмотр + Предпросмотр фона Скорость (%.2fx) Оценили: %.1f Новое обновление найдено! @@ -48,31 +48,31 @@ Поиск %s… Нет данных Дополнительные опции - Следующий серия + Следующий эпизод Жанры Поделиться - Открыть в браузер + Открыть в браузере Пропустить загрузку - Смотрю + Просмотр Приостановленно Завершено Брошенный - План по смотреть + План посмотреть Никто Пересмотрю Смотреть фильм - Проиграть трейлер + Воспроизвести трейлер Воспроизвести Livestream Источники Субтитры - Проиграть серия + Воспроизвести эпизод Повторная попытка подключение… Вернуться - Скачали + Скачано Скачивание Скачать приостановленный Скачать начатый - Скачать отменено + Скачать отменённый Скачать выполнено Инфо Обновление началось @@ -81,8 +81,8 @@ Подробнее Фильтр закладки Закладки - Наносить - Прервать + Применить + Отмена Копия Закрыть Очистить @@ -116,8 +116,8 @@ Удалить файл Проиграть файл Внутренняя память - Скачать резюме - Приостановить скачать + Продолжить Скачать + Приостановить скачивание Отключить автоматическое информирование об ошибках Импортируйте шрифты поместив их в %s Продолжить смотреть @@ -174,7 +174,7 @@ Э Эпизоды не найдены Удалить файл - Возобновить + Продолжить -30 +30 Это будет удалено безвозвратно%s @@ -193,7 +193,7 @@ Другое Ошибка загрузки, проверьте разрешения хранилища Копировать ссылку - Автоматическая загрузка + Автоскачивание Загрузка. Зеркало Сезон Аниме приложение от тех же разработчиков @@ -217,7 +217,7 @@ Торрент Документальный Азиатская драма - Общий + Основные Провайдеры Макет Расширения @@ -240,7 +240,7 @@ Обновление не найдено Изменить размер Источник - Проверьте наличие обновления + Проверить обновления Клон сайта DNS через HTTPS Удалить сайт @@ -248,7 +248,7 @@ Синхронизация субтитров Добавить клон существующего сайта с другим URL-адресом Используется для обхода блокировок интернет провайдера - Путь загрузки + Путь скачивания учитывая бенен Обновить Основной цвет @@ -279,7 +279,7 @@ Используйте яркость системы в проигрывателе приложения вместо темного наложения Обновить состояние хода просмотра Данные сохранены - Дает вам результаты поиска, разделенные по провайдеру + Показывает результаты поиска, разделенные по провайдеру Поиск предварительных обновлений вместо полных выпусков Повторить процесс настройки Этот провайдер не поддерживает Chromecast @@ -304,7 +304,7 @@ Пропустить это обновление URL-адрес NGINX-сервера Создать учётную запись - Добавить трекинг + Добавить слежение Добавлено %s Синхронизировать Оценено @@ -350,7 +350,7 @@ Добавить учётную запись МойКрутойСайт example.com - Язык (en) + Код языка (ru) учётная запись Автоматически 127.0.0.1 @@ -393,8 +393,8 @@ Отключено: %d %s %s %s аутентифицировано - Не удалось перейти к %s - Максимум + Не удается логин на %s + Макс Минимум Очертание Тень @@ -408,4 +408,91 @@ Съешь ещё этих мягких французских булок, да выпей же чаю Рекомендуется Загружено %s + \@нить/аниме + \@нить/ova + Этикетка Dub + Сайт + Функции + Главное + Источник + Случайный + Скоро… + Этикетка Sub + Фон + Oтoбpaжeниe + Трейлер + %s (отключено) + Следующий + В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. +\n +\nИз-за безмозглой DMCA-атаки со стороны Sky UK Limited 🤮 мы не можем привязать сайт репозитория в приложении. +\n +\nПрисоединяйтесь к нашему Discord или ищите в интернете. + Недопустимые данные + Разрешение и название + Предыдущий + Разрешение + Браузер + Библиотека + Обновленный (старый - новый) + Алфавитный (А - Я) + Алфавитный (Я - А) + Выбрать библиотеку + Открыть с + Похоже, ваша библиотека пуста :( +\nВойдите в аккаунт с библиотекой или добавьте сериалы в локальную библиотеку + Сортировка + Открытый список + Рейтинг (высокий - низкий) + Рейтинг (низкий - высокий) + Обновленный (новый - старый) + Сортировать по + PackageInstaller + Кодировка субтитров + Загрузить из файла + Рейтинг: %s + Скачано %d %s + Все %s уже скачаны + Начата загрузка %d %s… + Не скачано: %d + Скачать все плагины из этого репозитория\? + Включен безопасный режим + Скачано: %d + Обновлено %d плагинов + Загрузить из интернета + Загрузка обновления приложения… + Недопустимый URL + Применить при перезапуске + Отчеты ошибках + Что вы хотите увидеть + Смотрите видео на этих языках + Скачано файл + Изображение постера + Пакетная загрузка + Скачайте список сайтов, который вы хотите использовать + Отображать Аниме с Дубляжом/Субтитрами + Включить NSFW на поддерживаемых провайдерах + Убрать скрытые субтитры из субтитров + Дополнительно + Изменить вид интерфейса, чтобы соответствовать устройству + Аудио дорожки + Это также удалит все плагины репозитория + Просмотреть репозитории сообщества + Видео дорожки + Все расширения были отключены из-за сбоя, чтобы помочь вам найти то, которое вызывает проблемы. + Повтор + Слишком много текста. Не удалось сохранить в буфер обмена. + Установка обновления приложения… + Не удалось установить новую версию приложения + Файл безопасного режима найден! +\nНе загружаются никакие расширения при запуске, пока файл не будет удален. + Приложение будет обновлено после выхода + Похоже, этот список пуст, попробуйте переключиться на другой + Все субтитры заглавными + Показывать всплывающие окна для пропуска вступления/заключения + Фильтровать по предпочитаемому языку медиа + Неверный ID + Ссылка на стрим + Отображать рандомную кнопку на Главной странице + Рандомная кнопка \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml new file mode 100644 index 00000000..97039233 --- /dev/null +++ b/app/src/main/res/values-sk/strings.xml @@ -0,0 +1,107 @@ + + + Našla sa nová aktualizácia! +\n%s -> %s + Výplň + %dh %dm + Epizóda %d bude vydaná za + %s Ep %d + Ďalšia epizóda + Žánre + Zdielať + Otvoriť v prehliadači + Preskočiť načítavanie + Hrajú: %s + CloudStream + Plagát + %dd %dh %dm + %dm + %d min + \@string/result_poster_img_des + Plagát epizódy + Hlavný plagát + Prehrať s CloudStream + Nastavenia + Hľadať %s… + Pokračovať v sťahovaní + Hodnotenie: %.1f + Ísť späť + Rýchlosť (%.2fx) + Zmeniť poskytovateľa + Domov + Hľadať + Hľadať… + Sťahovanie + Žiadne dáta + Zrušiť + Kopírovať + Zavrieť + Uložiť + Stiahnuť + Stiahnuté + Ďalšie možnosti + Zdroje + Ísť späť + Sťahovanie zlyhalo + Sťahovanie pozastavené + Sťahovanie dokončené + Chyba pri načítavaní odkazov + Aktualizácia spustená + Interné úložisko + Načítavanie… + Dokončené + Plánujem pozerať + Zakázať automatické nahlasovanie chýb + Viac informácií + Záložky + Prehrať film + Prehrať upútavku + Sťahovanie + Sťahovanie zrušené + Dab + Zmazať súbor + Žiadny + Tit + Opätovné sledovanie + Prehrať súbor + Info + Prehrať živý prenos + Titulky + Prehrať epizódu + Pozastaviť sťahovanie + Skryť + Filtrovať záložky + Odstrániť + Použiť + Sťahovanie spustené + Vyčistiť + Prehrať + Nastaviť stav sledovania + Rýchlosť prehrávania + Farba obrysu + Farba okna + Typ hrany + Nastavenia titulkov + Farba pozadia + Farba textu + Vyvýšenie titulkov + Hľadať pomocou poskytovateľov + Písmo + Hľadať pomocou typov + Automaticky vybrať jazyk + Jazyk titulkov + Veľkosť písma + Nedarovali ste žiadne benény + Podržaním obnovíte predvolené nastavenia + %d benénov darovaných vývojárom + Stiahnuť jazyky + Odstrániť + Tento poskytovateľ je torrent, odporúča sa VPN + Importovať písma ich umiestnením do %s + Viac informácií + \@string/home_play + Pokračovať v sledovaní + Na správne fungovanie tohto poskytovateľa môže byť potrebná VPN + Stránka neposkytla žiadne metadáta, načítanie videa zlyhá, ak na stránke neexistuje. + Popis + \ No newline at end of file diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index 8bb22e66..b944b6b3 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -4,7 +4,7 @@ %dm %ds %dd %ds %dd %dd - %s Ep %d + %s Xlq %d Xalqadda %d waxa lasoo deyn doonaa Daji Dejintii ma guulaysan diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 03eee72b..bd245a61 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -13,7 +13,7 @@ %d %s Ep %d Cast: %s - Bölüm %d şu tarihte yayınlanacak: + Bölüm %d şu tarihte yayınlanacak %dd %dh %dm %dh %dm %dm @@ -435,18 +435,18 @@ Kurulumu atla Cihazınıza uygun görünümü seçin Çökme raporları - Ne izlemek istiyorsunuz\? + Ne izlemek istiyorsunuz Bitti Eklentiler Depo ekle Depo ismi - Depo URL\'i + Depo URL\'si Eklenti yüklendi Eklenti silindi %s yüklenemedi +18 - %d %s indirilmeye başlandı - %d %s başarıyla indirildi + %d %s … indirilmeye başlandı + %d %s indirildi %s\'nin tamamı zaten indirildi Toplu indir eklenti @@ -458,7 +458,11 @@ Devre dışı: %d İndirilmeyen: %d %d eklenti(ler) güncellendi - Site eklentilerini yüklemek için bir depo ekleyin + CloudStream\'in varsayılan olarak yüklü sitesi yoktur. Siteleri depolardan kurmanız gerekir. +\n +\nSky UK Limited tarafından beyinsiz bir DMCA yayından kaldırma 🤮 nedeniyle uygulama içinde depo linklerini bulunduramıyoruz. +\n +\nDiscord\'umuza katılın veya çevrimiçi arama yapın. Topluluk depolarını görüntüle Herkese açık liste Tüm alt yazılar büyük harf @@ -525,4 +529,12 @@ Tüm diller Geç %s İzlenenlerden kaldır + Karışık son + Karışık başlangıç + Kredi + Giriş + Eklenti İndirildi + Aksiyonlar + Açma/bitiş için atlama açılır pencerelerini göster + Çok fazla metin. Panoya kaydedilemiyor. \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 93e51c84..821d062a 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -489,4 +489,19 @@ Програму не знайдено Змішаний опенінг Видалити з переглянутого + За оновленням (від старого до нового) + За оновленням (від нового до старого) + Бібліотека + Сортувати + За рейтингом (від високого до низького) + Сортувати за + За алфавітом (від А до Я) + За рейтингом (від низького до високого) + Схоже, ваша бібліотека порожня :( +\nУвійдіть в обліковий запис бібліотеки або додайте серіали до вашої локальної бібліотеки + За алфавітом (від Я до А) + Виберіть бібліотеку + Відкрити з + Браузер + Схоже, цей список порожній, спробуйте перейти до іншого \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 8b70569a..db647b5d 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -66,7 +66,7 @@ Tải thành công Trực tiếp Đã có lỗi xảy ra - Bộ nhớ máy + Bộ nhớ trong Lồng Tiếng Phụ Đề Xóa Tệp @@ -113,13 +113,12 @@ \@string/home_play Bạn có thể sẽ cần sử dụng VPN để xem phim này Phim này được chiếu dưới dạng Torrent. Hãy sử dụng VPN để xem - Siêu dữ liệu không được cung cấp bởi trang web, quá trình tải video sẽ không thành công nếu nó không tồn tại trên trang web. Thông tin phim Đang cập nhật Không tìm thấy thông tin Hiển thị Logcat 🐈 Chế độ cửa sổ nhỏ - Tiếp tục xem phim khi thoát app hoặc đang tìm kiếm + Tiếp tục xem phim khi thoát ứng dụng hoặc khi tìm kiếm Bật nút thu phóng khi xem Xóa khoảng đen của phim Phụ đề @@ -160,7 +159,7 @@ Không gửi dữ liệu Hiển thị tập phụ cho anime Hiển thị trailer - Hiển thị poster từ kitsu + Hiển thị poster từ Kitsu Ẩn chất lượng video khi tìm kiếm Tự động cập nhật plugin Hiển thị thông báo cập nhật App @@ -211,7 +210,7 @@ Không có phụ đề Mặc Định Còn trống - Đã dùng + Đã sử dụng App Phim Lẻ @@ -229,7 +228,7 @@ Phim Lẻ Phim Bộ Hoạt Hình - \@string/anime + Anime \@string/ova Torrent Phim Tài Liệu @@ -262,7 +261,7 @@ Khóa Thu Phóng Tuỳ chọn - Tập tiếp + Tua nhanh Không hiện lại Bỏ qua Cập nhật @@ -273,8 +272,8 @@ Thời lượng bộ nhớ đệm Dung lượng video cache Xoá hình ảnh và video - Sẽ gây lỗi nếu đặt quá cao. Không thay đổi nếu máy có dung lượng ram thấp, chẳng hạn như Android TV hoặc điện thoại cũ - Sẽ thể gây lỗi trên các máy có dung lượng lưu trữ thấp, chẳng hạn như thiết bị Android TV nếu bạn đặt nó quá cao + Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng ram thấp như Android TV. + Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng lưu trữ thấp như Android TV. DNS over HTTPS Rất hữu ích để bỏ chặn ISP Sao chép trang web @@ -411,7 +410,7 @@ Đã xoá plugin Không tải được %s 18+ - Bắt đầu tải %d %s + Bắt đầu tải %d %s… Tải xuống %d %s thành công Toàn bộ %s đã được tải xuống Tải hàng loạt @@ -423,7 +422,11 @@ Đã tải: %d Đã vô hiệu: %d Không tải: %d - Thêm kho lưu trữ để cài tiện ích + CloudStream không có sẵn trang web nào. Bạn cần cài đặt các trang web từ kho lưu trữ. +\n +\nDo Sky UK Limited đã gỡ xuống theo DMCA một cách thiếu suy nghĩ 🤮 chúng tôi không thể cài sẵn trang web. +\n +\nHãy tham gia Discord của chúng tôi hoặc tìm kiếm trực tuyến. Xem kho lưu trữ của cộng đồng Danh sách công khai In hoa toàn bộ phụ đề @@ -438,13 +441,82 @@ Xem thông tin sự cố Lịch sử Đánh dấu là đã xem - Tự động tải plugin + Tự động tải xuống plugin Thiết lập lại Bộ cài APK Một số máy không hỗ trợ trình cài đặt gói mới. Hãy thử tùy chọn cũ nếu các bản cập nhật không cài đặt. %s %d%s - Xem Trailer - Tự động tải plugins còn thiếu. + Xem giới thiệu + Tự động tải plugin còn thiếu. Bắt đầu cập nhật Liên kết + Danh sách HLS + Trình phát ưu tiên + Trình phát mặc định + Đánh giá: %s + Không + Phiên bản + Tác giả + Cập nhật ứng dụng + Sao lưu + Tiện ích + Hành động + Cache + Cử chỉ + Tính năng trình phát + Phụ đề + Bố cục + Mặc định + Giao diện + Tính năng + Đã cập nhật %d plugin + Mô tả + Trạng thái + Kích thước + Hỗ trợ + Ngôn ngữ + Cài đặt tiện ích trước + VLC + MPV + Web Video Cast + Trình duyệt web + Không thấy ứng dụng + Tất cả ngôn ngữ + Tua %s + Mở đầu + Kết thúc + Tóm tắt + Mở đầu tuỳ chọn + Kết thúc tuỳ chọn + Danh đề + Giới thiệu + Xoá lịch sử + Hiển thị nút tua nhanh cho mở đầu/kết thúc + Văn bản quá dài. Không thể lưu vào bộ nhớ tạm. + Xoá khỏi đã xem + Bạn có chắc muốn thoát\? + + Đang tải bản cập nhật… + Đang cài bản cập nhật… + Không thể cài đặt phiên bản mới + Ứng dụng sẽ được cập nhật khi thoát + Thư viện + Trình duyệt + Plugin đã tải + Mặc định + Tải lên (Mới đến Cũ) + Tải lên (Cũ đến Mới) + Thư viện của bạn đang trống :( +\nHãy đăng nhập vào thư viện hoặc thêm phim vào thư viện cục bộ + Mở với + Siêu dữ liệu không có sẵn, video sẽ không được tải nếu nó không tồn tại trên trang web. + PackageInstaller + Sắp xếp + Xếp hạng (Cao đến Thấp) + Xếp hạng (Thấp đến Cao) + Chữ cái (Z đến A) + Sắp xếp + Có vẻ như danh sách này trống, hãy thử chuyển sang danh sách khác + Chữ cái (A đến Z) + Chọn Thư viện \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 97a48597..ece917d9 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -537,4 +537,21 @@ 应用退出后将会更新 插件已下载 从已观看中移除 + 发现安全模式文件! +\n启动时不加载任何扩展,直到文件被删除。 + 浏览器 + + 排序方式 + 排序 + 评分(从高到低) + 评分(从低到高) + 更新(从新到旧) + 更新(从旧到新) + 字母排序(从 A 到 Z) + 字母排序(从 Z 到 A) + 选择库 + 打开方式 + 看来您的库是空的 :( +\n登录库账户或添加节目到您的本地库 + 看来此列表是空的,请尝试切换到另一个 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 85eb3a48..61ff0c2b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -36,6 +36,8 @@ #F53B66 #BEC8FF ?attr/colorPrimaryDark + #4C3115 + #FFA662 #FF6F63 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 301b8313..f623add1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -112,6 +112,7 @@ Genres Share Open In Browser + Browser Skip Loading Loading… Watching @@ -231,6 +232,7 @@ Storage permissions missing. Please try again. Error backing up %s Search + Library Accounts Updates and backup Info @@ -623,5 +625,17 @@ Legacy PackageInstaller App will be updated upon exit - + Sort by + Sort + Rating (High to Low) + Rating (Low to High) + Updated (New to Old) + Updated (Old to New) + Alphabetical (A to Z) + Alphabetical (Z to A) + Select Library + Open with + Looks like your library is empty :(\nLogin to a library account or add shows to your local library + Looks like this list is empty, try switching to another one + Safe mode file found!\nNot loading any extensions on startup until file is removed. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b14cd189..2540bf34 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -90,11 +90,13 @@ @font/google_sans 0dp + + + + + +