From 3c82548c20a1ba523e8a5a76e4734c1620565bfb Mon Sep 17 00:00:00 2001 From: LagradOst <46196380+Blatzar@users.noreply.github.com> Date: Sat, 28 Jan 2023 22:38:02 +0000 Subject: [PATCH] Library merge (#343) --- app/build.gradle.kts | 3 + .../com/lagradost/cloudstream3/MainAPI.kt | 25 ++ .../lagradost/cloudstream3/MainActivity.kt | 6 + .../metaproviders/AnilistRedirector.kt | 30 -- .../metaproviders/SyncRedirector.kt | 56 +++ .../syncproviders/AccountManager.kt | 3 +- .../cloudstream3/syncproviders/SyncAPI.kt | 88 +++- .../cloudstream3/syncproviders/SyncRepo.kt | 22 +- .../syncproviders/providers/AniListApi.kt | 133 ++++-- .../syncproviders/providers/LocalList.kt | 100 +++++ .../syncproviders/providers/MALApi.kt | 135 ++++-- .../cloudstream3/ui/AutofitRecyclerView.kt | 5 +- .../cloudstream3/ui/home/HomeFragment.kt | 4 +- .../ui/library/LibraryFragment.kt | 393 ++++++++++++++++++ .../ui/library/LibraryScrollTransformer.kt | 17 + .../ui/library/LibraryViewModel.kt | 104 +++++ .../ui/library/LoadingPosterAdapter.kt | 37 ++ .../cloudstream3/ui/library/PageAdapter.kt | 130 ++++++ .../ui/library/ViewpagerAdapter.kt | 90 ++++ .../ui/quicksearch/QuickSearchFragment.kt | 2 +- .../cloudstream3/ui/result/ResultFragment.kt | 8 +- .../ui/result/ResultFragmentPhone.kt | 2 +- .../ui/result/ResultFragmentTv.kt | 2 +- .../ui/result/ResultViewModel2.kt | 21 +- .../cloudstream3/ui/search/SearchAdaptor.kt | 4 + .../cloudstream3/ui/search/SearchFragment.kt | 2 +- .../ui/search/SearchResultBuilder.kt | 5 +- .../ui/settings/extensions/PluginsFragment.kt | 2 +- .../lagradost/cloudstream3/utils/AppUtils.kt | 55 +++ .../cloudstream3/utils/BackupUtils.kt | 4 - .../cloudstream3/utils/DataStoreHelper.kt | 22 +- .../lagradost/cloudstream3/utils/SyncUtil.kt | 27 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 54 ++- app/src/main/res/color/item_select_color.xml | 2 + .../ic_baseline_collections_bookmark_24.xml | 6 + .../main/res/drawable/ic_baseline_sort_24.xml | 5 + .../main/res/drawable/ic_baseline_star_24.xml | 4 +- .../drawable/ic_outline_account_circle_24.xml | 6 + .../res/drawable/indicator_background.xml | 6 + app/src/main/res/drawable/rating_bg_color.xml | 6 + app/src/main/res/layout/activity_main.xml | 4 +- app/src/main/res/layout/fragment_library.xml | 177 ++++++++ .../res/layout/library_viewpager_page.xml | 11 + .../res/layout/loading_poster_dynamic.xml | 37 ++ .../layout/search_result_grid_expanded.xml | 131 ++++-- app/src/main/res/menu/bottom_nav_menu.xml | 29 +- app/src/main/res/menu/library_menu.xml | 17 + .../main/res/navigation/mobile_navigation.xml | 9 + app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 15 +- app/src/main/res/values/styles.xml | 15 + 51 files changed, 1855 insertions(+), 218 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt create mode 100644 app/src/main/res/drawable/ic_baseline_collections_bookmark_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_sort_24.xml create mode 100644 app/src/main/res/drawable/ic_outline_account_circle_24.xml create mode 100644 app/src/main/res/drawable/indicator_background.xml create mode 100644 app/src/main/res/drawable/rating_bg_color.xml create mode 100644 app/src/main/res/layout/fragment_library.xml create mode 100644 app/src/main/res/layout/library_viewpager_page.xml create mode 100644 app/src/main/res/layout/loading_poster_dynamic.xml create mode 100644 app/src/main/res/menu/library_menu.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1cbcec68..808c0cc3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) { 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..5720b7a7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -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 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/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/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/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/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 @@ --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/library_viewpager_page.xml b/app/src/main/res/layout/library_viewpager_page.xml new file mode 100644 index 00000000..f69f68b5 --- /dev/null +++ b/app/src/main/res/layout/library_viewpager_page.xml @@ -0,0 +1,11 @@ + + + + + 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/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" /> + + #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 6c10d865..cee6bccc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -110,6 +110,7 @@ Genres Share Open In Browser + Browser Skip Loading Loading… Watching @@ -229,6 +230,7 @@ Storage permissions missing. Please try again. Error backing up %s Search + Library Accounts Updates and backup Info @@ -616,5 +618,16 @@ 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 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 + + + + + +