forked from recloudstream/cloudstream
Merge branch 'anilist' into library
This commit is contained in:
commit
9edb7f8999
37 changed files with 1182 additions and 97 deletions
|
@ -13,8 +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.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
|
@ -459,6 +461,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<SyncIdName>()
|
||||
|
||||
open val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
|
@ -529,6 +545,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*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -1,10 +1,26 @@
|
|||
package com.lagradost.cloudstream3.syncproviders
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
|
||||
enum class SyncIdName {
|
||||
Anilist,
|
||||
MyAnimeList,
|
||||
Trakt,
|
||||
Imdb,
|
||||
LocalList
|
||||
}
|
||||
|
||||
interface SyncAPI : OAuth2API {
|
||||
val mainUrl: String
|
||||
|
||||
/**
|
||||
* Allows certain providers to open pages from
|
||||
* library links.
|
||||
**/
|
||||
val syncIdName: SyncIdName
|
||||
|
||||
/**
|
||||
-1 -> None
|
||||
0 -> Watching
|
||||
|
@ -22,7 +38,9 @@ interface SyncAPI : OAuth2API {
|
|||
|
||||
suspend fun search(name: String): List<SyncSearchResult>?
|
||||
|
||||
fun getIdFromUrl(url : String) : String
|
||||
suspend fun getPersonalLibrary(): LibraryMetadata?
|
||||
|
||||
fun getIdFromUrl(url: String): String
|
||||
|
||||
data class SyncSearchResult(
|
||||
override val name: String,
|
||||
|
@ -42,7 +60,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 +81,9 @@ interface SyncAPI : OAuth2API {
|
|||
var genres: List<String>? = null,
|
||||
var synonyms: List<String>? = null,
|
||||
var trailers: List<String>? = 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 +94,53 @@ interface SyncAPI : OAuth2API {
|
|||
var prevSeason: SyncSearchResult? = null,
|
||||
var actors: List<ActorData>? = null,
|
||||
)
|
||||
|
||||
|
||||
data class Page(
|
||||
val title: String, var items: List<LibraryItem>
|
||||
) {
|
||||
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()
|
||||
else -> items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LibraryMetadata(
|
||||
/** List of all available pages, useful to show empty pages
|
||||
* if the user has no entry on that page */
|
||||
val allListNames: List<String>,
|
||||
/** Not necessarily sorted list of all library items, will be grouped by listName */
|
||||
val allLibraryItems: List<LibraryItem>
|
||||
)
|
||||
|
||||
data class LibraryItem(
|
||||
override val name: String,
|
||||
override val url: String,
|
||||
/** Unique unchanging string used for data storage */
|
||||
val syncId: String,
|
||||
val listName: String,
|
||||
val episodesCompleted: Int?,
|
||||
val episodesTotal: Int?,
|
||||
/** Out of 100 */
|
||||
val personalRating: Int?,
|
||||
override val apiName: String,
|
||||
override var type: TvType?,
|
||||
override var posterUrl: String?,
|
||||
override var posterHeaders: Map<String, String>?,
|
||||
override var quality: SearchQuality?,
|
||||
override var id: Int? = null,
|
||||
) : SearchResponse
|
||||
}
|
|
@ -11,26 +11,33 @@ class SyncRepo(private val repo: SyncAPI) {
|
|||
val icon = repo.icon
|
||||
val mainUrl = repo.mainUrl
|
||||
val requiresLogin = repo.requiresLogin
|
||||
val syncIdName = repo.syncIdName
|
||||
|
||||
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
|
||||
return safeApiCall { repo.score(id, status) }
|
||||
}
|
||||
|
||||
suspend fun getStatus(id : String) : Resource<SyncAPI.SyncStatus> {
|
||||
suspend fun getStatus(id: String): Resource<SyncAPI.SyncStatus> {
|
||||
return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") }
|
||||
}
|
||||
|
||||
suspend fun getResult(id : String) : Resource<SyncAPI.SyncResult> {
|
||||
suspend fun getResult(id: String): Resource<SyncAPI.SyncResult> {
|
||||
return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") }
|
||||
}
|
||||
|
||||
suspend fun search(query : String) : Resource<List<SyncAPI.SyncSearchResult>> {
|
||||
suspend fun search(query: String): Resource<List<SyncAPI.SyncSearchResult>> {
|
||||
return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() }
|
||||
}
|
||||
|
||||
fun hasAccount() : Boolean {
|
||||
suspend fun getPersonalLibrary(): Resource<SyncAPI.LibraryMetadata> {
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -3,8 +3,8 @@ package com.lagradost.cloudstream3.syncproviders.providers
|
|||
import androidx.fragment.app.FragmentActivity
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
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.openBrowser
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -12,6 +12,7 @@ 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.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
|
@ -31,6 +32,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
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)?.
|
||||
|
@ -140,7 +142,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()) {
|
||||
|
@ -219,7 +222,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
romaji
|
||||
}
|
||||
idMal
|
||||
coverImage { medium large }
|
||||
coverImage { medium large extraLarge }
|
||||
averageScore
|
||||
}
|
||||
}
|
||||
|
@ -232,7 +235,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
format
|
||||
id
|
||||
idMal
|
||||
coverImage { medium large }
|
||||
coverImage { medium large extraLarge }
|
||||
averageScore
|
||||
title {
|
||||
english
|
||||
|
@ -575,7 +578,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 +606,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(listName: String?): 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(),
|
||||
listName?.lowercase()?.capitalize() ?: return null,
|
||||
this.progress,
|
||||
this.media.episodes,
|
||||
this.score,
|
||||
"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 +643,47 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
|
||||
)
|
||||
|
||||
fun getAnilistListCached(): Array<Lists>? {
|
||||
private fun getAniListListCached(): Array<Lists>? {
|
||||
return getKey(ANILIST_CACHED_LIST) as? Array<Lists>
|
||||
}
|
||||
|
||||
suspend fun getAnilistAnimeListSmart(): Array<Lists>? {
|
||||
private suspend fun getAniListAnimeListSmart(): Array<Lists>? {
|
||||
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()
|
||||
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<AniListUser>(key, null)?.let {
|
||||
userID = it.id
|
||||
}
|
||||
}
|
||||
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
|
||||
return SyncAPI.LibraryMetadata(
|
||||
emptyList(),
|
||||
getAniListAnimeListSmart()?.map {
|
||||
it.entries.mapNotNull { entry ->
|
||||
entry.toLibraryItem(
|
||||
entry.status ?: it.status
|
||||
)
|
||||
}
|
||||
}?.flatten() ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
val fixedUserID = userID ?: return null
|
||||
private suspend fun getFullAniListList(): FullAnilistList? {
|
||||
/** WARNING ASSUMES ONE USER! **/
|
||||
|
||||
val userID = getKey<AniListUser>(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
|
||||
|
@ -677,7 +710,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
english
|
||||
romaji
|
||||
}
|
||||
coverImage { medium }
|
||||
coverImage { extraLarge large medium }
|
||||
synonyms
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
|
@ -689,7 +722,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
}
|
||||
"""
|
||||
val text = postApi(query)
|
||||
val text = postApi(query).also {
|
||||
println("REPONSE $it")
|
||||
}
|
||||
return text?.toKotlinObject()
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import com.lagradost.cloudstream3.AcraApplication
|
||||
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.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 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() {
|
||||
}
|
||||
|
||||
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<SyncAPI.SyncSearchResult>? {
|
||||
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.mapNotNull {
|
||||
getBookmarkedData(it.first)?.toLibraryItem(it.second)
|
||||
}
|
||||
}
|
||||
|
||||
return SyncAPI.LibraryMetadata(
|
||||
WatchType.values().mapNotNull {
|
||||
// None is not something to display
|
||||
if (it == WatchType.NONE) return@mapNotNull null
|
||||
|
||||
// Dirty hack for context!
|
||||
txt(it.stringRes).asStringNull(AcraApplication.context)
|
||||
},
|
||||
list
|
||||
)
|
||||
}
|
||||
|
||||
override fun getIdFromUrl(url: String): String {
|
||||
return url
|
||||
}
|
||||
}
|
|
@ -8,11 +8,13 @@ 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.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
||||
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
||||
|
@ -34,6 +36,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
val apiUrl = "https://api.myanimelist.net"
|
||||
override val icon = R.drawable.mal_logo
|
||||
override val requiresLogin = false
|
||||
override val syncIdName = SyncIdName.MyAnimeList
|
||||
|
||||
override val createAccountUrl = "$mainUrl/register.php"
|
||||
|
||||
|
@ -382,7 +385,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?.status?.lowercase()?.capitalize()?.replace("_", " ") ?: "NONE",
|
||||
this.list_status?.num_episodes_watched,
|
||||
this.node.num_episodes,
|
||||
this.list_status?.score,
|
||||
"MAL",
|
||||
TvType.Anime,
|
||||
this.node.main_picture?.large ?: this.node.main_picture?.medium,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Paging(
|
||||
@JsonProperty("next") val next: String?
|
||||
|
@ -425,6 +445,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
|
||||
return SyncAPI.LibraryMetadata(
|
||||
emptyList(),
|
||||
getMalAnimeListSmart()?.map { it.toLibraryItem() } ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getMalAnimeList(): Array<Data> {
|
||||
checkMalToken()
|
||||
var offset = 0
|
||||
|
|
|
@ -0,0 +1,268 @@
|
|||
package com.lagradost.cloudstream3.ui.library
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.SearchView
|
||||
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.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
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.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import kotlinx.android.synthetic.main.fragment_library.*
|
||||
|
||||
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),
|
||||
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()
|
||||
}
|
||||
|
||||
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 onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.fixPaddingStatusbar(library_root)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
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(
|
||||
txt(LibraryOpenerType.Default.stringRes).asString(this),
|
||||
txt(LibraryOpenerType.None.stringRes).asString(this),
|
||||
txt(LibraryOpenerType.Browser.stringRes).asString(this),
|
||||
)
|
||||
|
||||
val items = baseOptions + availableProviders
|
||||
|
||||
val savedSelection = getKey<LibraryOpener>(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.name)
|
||||
}
|
||||
|
||||
this.showBottomDialog(
|
||||
items,
|
||||
selectedIndex,
|
||||
txt(R.string.open_with).asString(this),
|
||||
true,
|
||||
{},
|
||||
) {
|
||||
val savedData = if (it < baseOptions.size) {
|
||||
LibraryOpener(
|
||||
LibraryOpenerType.valueOf(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<LibraryOpener>(LIBRARY_FOLDER, syncName.name)
|
||||
val savedSelection = getKey<LibraryOpener>(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
|
||||
)
|
||||
}
|
||||
}
|
||||
LibraryOpenerType.None -> {}
|
||||
LibraryOpenerType.Provider ->
|
||||
savedSelection.providerData?.apiName?.let { apiName ->
|
||||
activity?.loadResult(
|
||||
searchClickCallback.card.url,
|
||||
apiName,
|
||||
)
|
||||
}
|
||||
LibraryOpenerType.Browser ->
|
||||
openBrowser(searchClickCallback.card.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
viewpager?.offscreenPageLimit = 2
|
||||
|
||||
observe(libraryViewModel.pages) { pages ->
|
||||
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
|
||||
// Using notifyItemRangeChanged keeps the animations when sorting
|
||||
viewpager.adapter?.notifyItemRangeChanged(0, viewpager.adapter?.itemCount ?: 0)
|
||||
|
||||
TabLayoutMediator(
|
||||
library_tab_layout,
|
||||
viewpager,
|
||||
) { tab, position ->
|
||||
tab.text = pages.getOrNull(position)?.title
|
||||
}.attach()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MenuSearchView(context: Context) : SearchView(context) {
|
||||
override fun onActionViewCollapsed() {
|
||||
super.onActionViewCollapsed()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.lagradost.cloudstream3.ui.library
|
||||
|
||||
import android.view.View
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import kotlinx.android.synthetic.main.library_viewpager_page.view.*
|
||||
|
||||
class LibraryScrollTransformer : ViewPager2.PageTransformer {
|
||||
override fun transformPage(page: View, position: Float) {
|
||||
val padding = (-position * page.width).toInt()
|
||||
page.page_recyclerview.setPadding(
|
||||
padding, 0,
|
||||
-padding, 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
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.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
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
class LibraryViewModel : ViewModel() {
|
||||
private val _pages: MutableLiveData<List<SyncAPI.Page>> = MutableLiveData(emptyList())
|
||||
val pages: LiveData<List<SyncAPI.Page>> = _pages
|
||||
|
||||
private val _currentApiName: MutableLiveData<String> = MutableLiveData("")
|
||||
val currentApiName: LiveData<String> = _currentApiName
|
||||
|
||||
private val availableSyncApis = SyncApis.filter { it.hasAccount() }
|
||||
|
||||
// TODO REMEMBER SELECTION
|
||||
var currentSyncApi = availableSyncApis.firstOrNull()
|
||||
private set
|
||||
|
||||
val availableApiNames: List<String> = availableSyncApis.map { it.name }
|
||||
|
||||
val sortingMethods = listOf(
|
||||
ListSorting.RatingHigh,
|
||||
ListSorting.RatingLow,
|
||||
// ListSorting.UpdatedNew,
|
||||
// ListSorting.UpdatedOld,
|
||||
ListSorting.AlphabeticalA,
|
||||
ListSorting.AlphabeticalZ,
|
||||
)
|
||||
|
||||
var currentSortingMethod: ListSorting = sortingMethods.first().also {
|
||||
println("SET SORTING METHOD $it")
|
||||
}
|
||||
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.forEachIndexed { index, 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?.isNotEmpty() == true) return
|
||||
|
||||
ioSafe {
|
||||
currentSyncApi?.let { repo ->
|
||||
_currentApiName.postValue(repo.name)
|
||||
val library = (repo.getPersonalLibrary() as? Resource.Success)?.value ?: return@let
|
||||
|
||||
val listSubset = library.allLibraryItems.groupBy { it.listName }
|
||||
val allLists = library.allListNames.associateWith { emptyList<SyncAPI.LibraryItem>() }
|
||||
|
||||
val filledLists = allLists + listSubset
|
||||
|
||||
val pages = filledLists.map {
|
||||
SyncAPI.Page(
|
||||
it.key,
|
||||
it.value
|
||||
)
|
||||
}
|
||||
_pages.postValue(pages)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package com.lagradost.cloudstream3.ui.library
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import kotlinx.android.synthetic.main.search_result_grid_expanded.view.*
|
||||
|
||||
class PageAdapter(
|
||||
override val items: MutableList<SyncAPI.LibraryItem>,
|
||||
val clickCallback: (SearchClickCallback) -> Unit
|
||||
) :
|
||||
AppUtils.DiffAdapter<SyncAPI.LibraryItem>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
fun bind(item: SyncAPI.LibraryItem, position: Int) {
|
||||
SearchResultBuilder.bind(
|
||||
this@PageAdapter.clickCallback,
|
||||
item,
|
||||
position,
|
||||
itemView,
|
||||
)
|
||||
|
||||
// Set watch progress bar
|
||||
// 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.isVisible = showRating
|
||||
if (showRating) {
|
||||
itemView.text_rating.text = item.personalRating.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package com.lagradost.cloudstream3.ui.library
|
||||
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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<SyncAPI.Page>,
|
||||
val scrollCallback: (isScrollingDown: Boolean) -> Unit,
|
||||
val clickCallback: (SearchClickCallback) -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class PageViewHolder(private val itemViewTest: View) :
|
||||
RecyclerView.ViewHolder(itemViewTest) {
|
||||
fun bind(page: SyncAPI.Page) {
|
||||
if (itemViewTest.page_recyclerview?.adapter == null) {
|
||||
itemViewTest.page_recyclerview?.adapter = PageAdapter(page.items.toMutableList(), clickCallback)
|
||||
itemView.page_recyclerview?.spanCount =
|
||||
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
|
||||
} 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
|
||||
}
|
||||
}
|
|
@ -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<SearchResponse>()
|
||||
// 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
|
||||
|
|
|
@ -15,7 +15,9 @@ 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
|
||||
|
|
|
@ -28,6 +28,7 @@ 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.*
|
||||
|
@ -65,6 +66,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) {
|
||||
|
@ -329,6 +331,46 @@ object AppUtils {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class DiffAdapter<T>(
|
||||
open val items: MutableList<T>,
|
||||
val comparison: (first: T, second: T) -> Boolean = { first, second ->
|
||||
first.hashCode() == second.hashCode()
|
||||
}
|
||||
) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
fun updateList(newList: List<T>) {
|
||||
val diffResult = DiffUtil.calculateDiff(
|
||||
GenericDiffCallback(this.items, newList)
|
||||
)
|
||||
|
||||
items.clear()
|
||||
items.addAll(newList)
|
||||
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
inner class GenericDiffCallback(
|
||||
private val oldList: List<T>,
|
||||
private val newList: List<T>
|
||||
) :
|
||||
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
|
||||
|
|
|
@ -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,7 @@ 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.SyncAPI
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.result.VideoWatchState
|
||||
|
||||
|
@ -51,7 +53,20 @@ object DataStoreHelper {
|
|||
@JsonProperty("year") val year: Int?,
|
||||
@JsonProperty("quality") override var quality: SearchQuality? = null,
|
||||
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
||||
) : SearchResponse
|
||||
) : SearchResponse {
|
||||
fun toLibraryItem(state: WatchType): SyncAPI.LibraryItem {
|
||||
return SyncAPI.LibraryItem(
|
||||
name,
|
||||
url,
|
||||
url,
|
||||
state.name.lowercase().capitalize(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
apiName, type, posterUrl, posterHeaders, quality, id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ResumeWatchingResult(
|
||||
@JsonProperty("name") override val name: String,
|
||||
|
@ -71,6 +86,9 @@ object DataStoreHelper {
|
|||
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = 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<Int>? {
|
||||
|
|
|
@ -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<String> {
|
||||
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<SyncPage>()
|
||||
// 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<String> {
|
||||
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<SyncPage>()
|
||||
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(
|
||||
|
|
|
@ -105,7 +105,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
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorPrimary" android:state_checked="true"/>
|
||||
<item android:color="?attr/colorPrimary" android:state_focused="true"/>
|
||||
<item android:color="?attr/colorPrimary" android:state_selected="true"/>
|
||||
<item android:color="?attr/grayTextColor" android:state_checked="false"/>
|
||||
</selector>
|
|
@ -0,0 +1,6 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,12l-2.5,-1.5L15,12L15,4h5v8z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_baseline_sort_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_sort_24.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="?attr/white" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
|
||||
</vector>
|
|
@ -1,5 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="?attr/white"
|
||||
<vector android:height="12dp" android:tint="?attr/white"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
android:width="12dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
|
||||
</vector>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM7.35,18.5C8.66,17.56 10.26,17 12,17s3.34,0.56 4.65,1.5C15.34,19.44 13.74,20 12,20S8.66,19.44 7.35,18.5zM18.14,17.12L18.14,17.12C16.45,15.8 14.32,15 12,15s-4.45,0.8 -6.14,2.12l0,0C4.7,15.73 4,13.95 4,12c0,-4.42 3.58,-8 8,-8s8,3.58 8,8C20,13.95 19.3,15.73 18.14,17.12z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,6c-1.93,0 -3.5,1.57 -3.5,3.5S10.07,13 12,13s3.5,-1.57 3.5,-3.5S13.93,6 12,6zM12,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S11.17,8 12,8s1.5,0.67 1.5,1.5S12.83,11 12,11z"/>
|
||||
</vector>
|
6
app/src/main/res/drawable/indicator_background.xml
Normal file
6
app/src/main/res/drawable/indicator_background.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/textColor"/>
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
6
app/src/main/res/drawable/rating_bg_color.xml
Normal file
6
app/src/main/res/drawable/rating_bg_color.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/ratingColorBg"/>
|
||||
<corners android:radius="@dimen/rounded_image_radius"/>
|
||||
<!-- <stroke android:color="@color/subColor" android:width="2dp"/>-->
|
||||
</shape>
|
141
app/src/main/res/layout/fragment_library.xml
Normal file
141
app/src/main/res/layout/fragment_library.xml
Normal file
|
@ -0,0 +1,141 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/library_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/primaryGrayBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<ImageView
|
||||
android:id="@+id/provider_selector"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
|
||||
android:layout_marginStart="10dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/change_providers_img_des"
|
||||
android:src="@drawable/ic_baseline_extension_24"
|
||||
app:tint="?attr/textColor" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="10dp"
|
||||
android:background="@drawable/search_background"
|
||||
android:visibility="visible"
|
||||
app:layout_scrollFlags="scroll|enterAlways">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/main_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
|
||||
android:iconifiedByDefault="false"
|
||||
android:imeOptions="actionSearch"
|
||||
|
||||
android:inputType="text"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
|
||||
android:nextFocusRight="@id/search_filter"
|
||||
android:paddingStart="-10dp"
|
||||
app:iconifiedByDefault="false"
|
||||
app:queryBackground="@color/transparent"
|
||||
app:queryHint="@string/search_hint"
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
</androidx.appcompat.widget.SearchView>
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/list_selector"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
|
||||
android:layout_margin="10dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/change_providers_img_des"
|
||||
android:nextFocusLeft="@id/main_search"
|
||||
android:nextFocusRight="@id/main_search"
|
||||
android:src="@drawable/ic_baseline_filter_list_24"
|
||||
app:tint="?attr/textColor" />
|
||||
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<!-- <RelativeLayout-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent"-->
|
||||
<!-- android:nestedScrollingEnabled="true"-->
|
||||
<!-- android:orientation="vertical"-->
|
||||
<!-- app:layout_behavior="@string/appbar_scrolling_view_behavior">-->
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewpager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="40dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:listitem="@layout/library_viewpager_page">
|
||||
|
||||
</androidx.viewpager2.widget.ViewPager2>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="40dp">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/sort_fab"
|
||||
style="@style/ExtendedFloatingActionButton"
|
||||
android:text="Sort"
|
||||
android:textColor="?attr/textColor"
|
||||
app:icon="@drawable/ic_baseline_sort_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- </com.google.android.material.appbar.AppBarLayout>-->
|
||||
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/library_tab_layout"
|
||||
style="@style/Theme.Widget.Tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:focusable="false"
|
||||
|
||||
android:paddingHorizontal="5dp"
|
||||
app:layout_scrollFlags="noScroll"
|
||||
app:tabBackground="?attr/primaryGrayBackground"
|
||||
app:tabGravity="center"
|
||||
app:tabIndicator="@drawable/indicator_background"
|
||||
app:tabIndicatorColor="@color/textColor"
|
||||
app:tabIndicatorGravity="center"
|
||||
app:tabIndicatorHeight="30dp"
|
||||
app:tabMode="scrollable"
|
||||
app:tabSelectedTextColor="@color/lightTextColor"
|
||||
app:tabTextAppearance="@style/TabNoCaps"
|
||||
app:tabTextColor="@color/textColor" />
|
||||
<!-- </RelativeLayout>-->
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
11
app/src/main/res/layout/library_viewpager_page.xml
Normal file
11
app/src/main/res/layout/library_viewpager_page.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
|
||||
<com.lagradost.cloudstream3.ui.AutofitRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/page_recyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/home_result_grid_expanded" />
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
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"
|
||||
|
@ -78,4 +77,4 @@
|
|||
android:textColor="?attr/textColor"
|
||||
android:textSize="13sp"
|
||||
tools:text="The Perfect Run\nThe Perfect Run" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -6,15 +6,19 @@
|
|||
android:icon="@drawable/home_alt"
|
||||
android:title="@string/title_home"/>
|
||||
<item
|
||||
android:id="@+id/navigation_search"
|
||||
android:icon="@drawable/search_icon"
|
||||
android:title="@string/title_search"/>
|
||||
android:id="@+id/navigation_search"
|
||||
android:icon="@drawable/search_icon"
|
||||
android:title="@string/title_search" />
|
||||
<item
|
||||
android:id="@+id/navigation_downloads"
|
||||
android:icon="@drawable/netflix_download"
|
||||
android:title="@string/title_downloads"/>
|
||||
android:id="@+id/navigation_library"
|
||||
android:icon="@drawable/ic_outline_account_circle_24"
|
||||
android:title="@string/library" />
|
||||
<item
|
||||
android:id="@+id/navigation_settings"
|
||||
android:icon="@drawable/settings_alt"
|
||||
android:title="@string/title_settings"/>
|
||||
android:id="@+id/navigation_downloads"
|
||||
android:icon="@drawable/netflix_download"
|
||||
android:title="@string/title_downloads" />
|
||||
<item
|
||||
android:id="@+id/navigation_settings"
|
||||
android:icon="@drawable/ic_outline_settings_24"
|
||||
android:title="@string/title_settings" />
|
||||
</menu>
|
17
app/src/main/res/menu/library_menu.xml
Normal file
17
app/src/main/res/menu/library_menu.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/search_button"
|
||||
android:icon="@drawable/search_icon"
|
||||
android:title="@string/title_search"
|
||||
app:searchHintIcon="@drawable/search_icon"
|
||||
app:showAsAction="collapseActionView|ifRoom"
|
||||
app:actionViewClass="com.lagradost.cloudstream3.ui.library.MenuSearchView" />
|
||||
<item
|
||||
android:id="@+id/sort_button"
|
||||
android:icon="@drawable/ic_baseline_sort_24"
|
||||
android:title="Sort"
|
||||
app:showAsAction="collapseActionView|ifRoom" />
|
||||
|
||||
</menu>
|
|
@ -144,6 +144,15 @@
|
|||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_library"
|
||||
android:name="com.lagradost.cloudstream3.ui.library.LibraryFragment"
|
||||
android:label="@string/library"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_settings_general"
|
||||
android:name="com.lagradost.cloudstream3.ui.settings.SettingsGeneral"
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
<color name="subColorBg">#F53B66</color>
|
||||
<color name="typeColorText">#BEC8FF</color>
|
||||
<color name="typeColorBg">?attr/colorPrimaryDark</color>
|
||||
<color name="ratingColorBg">#3F51B5</color>
|
||||
|
||||
<color name="adultColor">#FF6F63</color> <!-- same as sub color -->
|
||||
|
||||
|
|
|
@ -110,6 +110,7 @@
|
|||
<string name="result_tags">Genres</string>
|
||||
<string name="result_share">Share</string>
|
||||
<string name="result_open_in_browser">Open In Browser</string>
|
||||
<string name="browser">Browser</string>
|
||||
<string name="skip_loading">Skip Loading</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="type_watching">Watching</string>
|
||||
|
@ -229,6 +230,7 @@
|
|||
<string name="backup_failed">Storage permissions missing. Please try again.</string>
|
||||
<string name="backup_failed_error_format">Error backing up %s</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="library">Library</string>
|
||||
<string name="category_account">Accounts</string>
|
||||
<string name="category_updates">Updates and backup</string>
|
||||
<string name="settings_info">Info</string>
|
||||
|
@ -616,5 +618,13 @@
|
|||
<string name="apk_installer_legacy">Legacy</string>
|
||||
<string name="apk_installer_package_installer">PackageInstaller</string>
|
||||
<string name="delayed_update_notice">App will be updated upon exit</string>
|
||||
|
||||
<string name="sort_by">Sort by</string>
|
||||
<string name="sort_rating_desc">Rating (High to Low)</string>
|
||||
<string name="sort_rating_asc">Rating (Low to High)</string>
|
||||
<string name="sort_updated_new">Updated (New to Old)</string>
|
||||
<string name="sort_updated_old">Updated (Old to New)</string>
|
||||
<string name="sort_alphabetical_a">Alphabetical (A to Z)</string>
|
||||
<string name="sort_alphabetical_z">Alphabetical (Z to A)</string>
|
||||
<string name="select_library">Select Library</string>
|
||||
<string name="open_with">Open with</string>
|
||||
</resources>
|
||||
|
|
|
@ -375,6 +375,11 @@
|
|||
<item name="textAllCaps">false</item>
|
||||
</style>
|
||||
|
||||
<style name="TabNoCaps" parent="TextAppearance.Design.Tab">
|
||||
<item name="textAllCaps">false</item>
|
||||
<item name="fontFamily">@font/google_sans</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTextViewStyle" parent="android:Widget.TextView">
|
||||
<item name="android:fontFamily">@font/google_sans</item>
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue