mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Library merge (#343)
This commit is contained in:
parent
9d11dc76a1
commit
3c82548c20
51 changed files with 1855 additions and 218 deletions
|
@ -220,6 +220,9 @@ dependencies {
|
||||||
|
|
||||||
// Library/extensions searching with Levenshtein distance
|
// Library/extensions searching with Levenshtein distance
|
||||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
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) {
|
tasks.register("androidSourcesJar", Jar::class) {
|
||||||
|
|
|
@ -13,7 +13,10 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
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.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.ui.result.UiText
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||||
|
@ -510,6 +513,20 @@ abstract class MainAPI {
|
||||||
open val hasMainPage = false
|
open val hasMainPage = false
|
||||||
open val hasQuickSearch = 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(
|
open val supportedTypes = setOf(
|
||||||
TvType.Movie,
|
TvType.Movie,
|
||||||
TvType.TvSeries,
|
TvType.TvSeries,
|
||||||
|
@ -580,6 +597,14 @@ abstract class MainAPI {
|
||||||
open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
|
open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
|
||||||
return null
|
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*/
|
/** Might need a different implementation for desktop*/
|
||||||
|
|
|
@ -388,6 +388,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
val isNavVisible = listOf(
|
val isNavVisible = listOf(
|
||||||
R.id.navigation_home,
|
R.id.navigation_home,
|
||||||
R.id.navigation_search,
|
R.id.navigation_search,
|
||||||
|
R.id.navigation_library,
|
||||||
R.id.navigation_downloads,
|
R.id.navigation_downloads,
|
||||||
R.id.navigation_settings,
|
R.id.navigation_settings,
|
||||||
R.id.navigation_download_child,
|
R.id.navigation_download_child,
|
||||||
|
@ -438,6 +439,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
|
|
||||||
nav_view?.isVisible = isNavVisible && !landscape
|
nav_view?.isVisible = isNavVisible && !landscape
|
||||||
nav_rail_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
|
//private var mCastSession: CastSession? = 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 openSubtitlesApi = OpenSubtitlesApi(0)
|
||||||
val indexSubtitlesApi = IndexSubtitleApi()
|
val indexSubtitlesApi = IndexSubtitleApi()
|
||||||
val addic7ed = Addic7ed()
|
val addic7ed = Addic7ed()
|
||||||
|
val localListApi = LocalList()
|
||||||
|
|
||||||
// used to login via app intent
|
// used to login via app intent
|
||||||
val OAuth2Apis
|
val OAuth2Apis
|
||||||
|
@ -29,7 +30,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
// used for active syncing
|
// used for active syncing
|
||||||
val SyncApis
|
val SyncApis
|
||||||
get() = listOf(
|
get() = listOf(
|
||||||
SyncRepo(malApi), SyncRepo(aniListApi)
|
SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi)
|
||||||
)
|
)
|
||||||
|
|
||||||
val inAppAuths
|
val inAppAuths
|
||||||
|
|
|
@ -1,10 +1,31 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders
|
package com.lagradost.cloudstream3.syncproviders
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.*
|
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 {
|
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
|
val mainUrl: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows certain providers to open pages from
|
||||||
|
* library links.
|
||||||
|
**/
|
||||||
|
val syncIdName: SyncIdName
|
||||||
|
|
||||||
/**
|
/**
|
||||||
-1 -> None
|
-1 -> None
|
||||||
0 -> Watching
|
0 -> Watching
|
||||||
|
@ -22,7 +43,9 @@ interface SyncAPI : OAuth2API {
|
||||||
|
|
||||||
suspend fun search(name: String): List<SyncSearchResult>?
|
suspend fun search(name: String): List<SyncSearchResult>?
|
||||||
|
|
||||||
fun getIdFromUrl(url : String) : String
|
suspend fun getPersonalLibrary(): LibraryMetadata?
|
||||||
|
|
||||||
|
fun getIdFromUrl(url: String): String
|
||||||
|
|
||||||
data class SyncSearchResult(
|
data class SyncSearchResult(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
|
@ -42,7 +65,7 @@ interface SyncAPI : OAuth2API {
|
||||||
val score: Int?,
|
val score: Int?,
|
||||||
val watchedEpisodes: Int?,
|
val watchedEpisodes: Int?,
|
||||||
var isFavorite: Boolean? = null,
|
var isFavorite: Boolean? = null,
|
||||||
var maxEpisodes : Int? = null,
|
var maxEpisodes: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SyncResult(
|
data class SyncResult(
|
||||||
|
@ -63,9 +86,9 @@ interface SyncAPI : OAuth2API {
|
||||||
var genres: List<String>? = null,
|
var genres: List<String>? = null,
|
||||||
var synonyms: List<String>? = null,
|
var synonyms: List<String>? = null,
|
||||||
var trailers: List<String>? = null,
|
var trailers: List<String>? = null,
|
||||||
var isAdult : Boolean? = null,
|
var isAdult: Boolean? = null,
|
||||||
var posterUrl: String? = null,
|
var posterUrl: String? = null,
|
||||||
var backgroundPosterUrl : String? = null,
|
var backgroundPosterUrl: String? = null,
|
||||||
|
|
||||||
/** In unixtime */
|
/** In unixtime */
|
||||||
var startDate: Long? = null,
|
var startDate: Long? = null,
|
||||||
|
@ -76,4 +99,61 @@ interface SyncAPI : OAuth2API {
|
||||||
var prevSeason: SyncSearchResult? = null,
|
var prevSeason: SyncSearchResult? = null,
|
||||||
var actors: List<ActorData>? = null,
|
var actors: List<ActorData>? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
data class Page(
|
||||||
|
val title: UiText, 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()
|
||||||
|
ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
|
||||||
|
ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
|
||||||
|
else -> items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LibraryMetadata(
|
||||||
|
val allLibraryLists: List<LibraryList>,
|
||||||
|
val supportedListSorting: Set<ListSorting>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LibraryList(
|
||||||
|
val name: UiText,
|
||||||
|
val items: List<LibraryItem>
|
||||||
|
)
|
||||||
|
|
||||||
|
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<String, String>?,
|
||||||
|
override var quality: SearchQuality?,
|
||||||
|
override var id: Int? = null,
|
||||||
|
) : SearchResponse
|
||||||
}
|
}
|
|
@ -11,26 +11,38 @@ class SyncRepo(private val repo: SyncAPI) {
|
||||||
val icon = repo.icon
|
val icon = repo.icon
|
||||||
val mainUrl = repo.mainUrl
|
val mainUrl = repo.mainUrl
|
||||||
val requiresLogin = repo.requiresLogin
|
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<Boolean> {
|
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
|
||||||
return safeApiCall { repo.score(id, status) }
|
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") }
|
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") }
|
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() }
|
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
|
return normalSafeApiCall { repo.loginInfo() != null } ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIdFromUrl(url : String) : String = repo.getIdFromUrl(url)
|
fun getIdFromUrl(url: String): String? = normalSafeApiCall {
|
||||||
|
repo.getIdFromUrl(url)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders.providers
|
package com.lagradost.cloudstream3.syncproviders.providers
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
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.openBrowser
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
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.AccountManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
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.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
|
@ -27,10 +30,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override val key = "6871"
|
override val key = "6871"
|
||||||
override val redirectUrl = "anilistlogin"
|
override val redirectUrl = "anilistlogin"
|
||||||
override val idPrefix = "anilist"
|
override val idPrefix = "anilist"
|
||||||
|
override var requireLibraryRefresh = true
|
||||||
override var mainUrl = "https://anilist.co"
|
override var mainUrl = "https://anilist.co"
|
||||||
override val icon = R.drawable.ic_anilist_icon
|
override val icon = R.drawable.ic_anilist_icon
|
||||||
override val requiresLogin = false
|
override val requiresLogin = false
|
||||||
override val createAccountUrl = "$mainUrl/signup"
|
override val createAccountUrl = "$mainUrl/signup"
|
||||||
|
override val syncIdName = SyncIdName.Anilist
|
||||||
|
|
||||||
override fun loginInfo(): AuthAPI.LoginInfo? {
|
override fun loginInfo(): AuthAPI.LoginInfo? {
|
||||||
// context.getUser(true)?.
|
// context.getUser(true)?.
|
||||||
|
@ -45,6 +50,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logOut() {
|
override fun logOut() {
|
||||||
|
requireLibraryRefresh = true
|
||||||
removeAccountKeys()
|
removeAccountKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,8 +70,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
switchToNewAccount()
|
switchToNewAccount()
|
||||||
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
|
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
|
||||||
setKey(accountId, ANILIST_TOKEN_KEY, token)
|
setKey(accountId, ANILIST_TOKEN_KEY, token)
|
||||||
setKey(ANILIST_SHOULD_UPDATE_LIST, true)
|
|
||||||
val user = getUser()
|
val user = getUser()
|
||||||
|
requireLibraryRefresh = true
|
||||||
return user != null
|
return user != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +146,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
this.name,
|
this.name,
|
||||||
recMedia.id?.toString() ?: return@mapNotNull null,
|
recMedia.id?.toString() ?: return@mapNotNull null,
|
||||||
getUrlFromId(recMedia.id),
|
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()) {
|
trailers = when (season.trailer?.site?.lowercase()?.trim()) {
|
||||||
|
@ -170,7 +177,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
fromIntToAnimeStatus(status.status),
|
fromIntToAnimeStatus(status.status),
|
||||||
status.score,
|
status.score,
|
||||||
status.watchedEpisodes
|
status.watchedEpisodes
|
||||||
)
|
).also {
|
||||||
|
requireLibraryRefresh = requireLibraryRefresh || it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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_TOKEN_KEY: String = "anilist_token" // anilist token for api
|
||||||
const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile
|
const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile
|
||||||
const val ANILIST_CACHED_LIST: String = "anilist_cached_list"
|
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 {
|
private fun fixName(name: String): String {
|
||||||
return name.lowercase(Locale.ROOT).replace(" ", "")
|
return name.lowercase(Locale.ROOT).replace(" ", "")
|
||||||
|
@ -219,7 +227,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
romaji
|
romaji
|
||||||
}
|
}
|
||||||
idMal
|
idMal
|
||||||
coverImage { medium large }
|
coverImage { medium large extraLarge }
|
||||||
averageScore
|
averageScore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,7 +240,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
format
|
format
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
coverImage { medium large }
|
coverImage { medium large extraLarge }
|
||||||
averageScore
|
averageScore
|
||||||
title {
|
title {
|
||||||
english
|
english
|
||||||
|
@ -292,15 +300,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val shows = searchShows(name.replace(blackListRegex, ""))
|
val shows = searchShows(name.replace(blackListRegex, ""))
|
||||||
|
|
||||||
shows?.data?.Page?.media?.find {
|
shows?.data?.Page?.media?.find {
|
||||||
malId ?: "NONE" == it.idMal.toString()
|
(malId ?: "NONE") == it.idMal.toString()
|
||||||
}?.let { return it }
|
}?.let { return it }
|
||||||
|
|
||||||
val filtered =
|
val filtered =
|
||||||
shows?.data?.Page?.media?.filter {
|
shows?.data?.Page?.media?.filter {
|
||||||
(
|
(((it.startDate.year ?: year.toString()) == year.toString()
|
||||||
it.startDate.year ?: year.toString() == year.toString()
|
|| year == null))
|
||||||
|| year == null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
filtered?.forEach {
|
filtered?.forEach {
|
||||||
it.title.romaji?.let { romaji ->
|
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
|
// Changing names of these will show up in UI
|
||||||
enum class AniListStatusType(var value: Int) {
|
enum class AniListStatusType(var value: Int, @StringRes val stringRes: Int) {
|
||||||
Watching(0),
|
Watching(0, R.string.type_watching),
|
||||||
Completed(1),
|
Completed(1, R.string.type_completed),
|
||||||
Paused(2),
|
Paused(2, R.string.type_on_hold),
|
||||||
Dropped(3),
|
Dropped(3, R.string.type_dropped),
|
||||||
Planning(4),
|
Planning(4, R.string.type_plan_to_watch),
|
||||||
ReWatching(5),
|
ReWatching(5, R.string.type_re_watching),
|
||||||
None(-1)
|
None(-1, R.string.none)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp }
|
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))
|
return fromIntToAnimeStatus(aniListStatusString.indexOf(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -526,7 +532,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
app.post(
|
app.post(
|
||||||
"https://graphql.anilist.co/",
|
"https://graphql.anilist.co/",
|
||||||
headers = mapOf(
|
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"
|
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
|
||||||
),
|
),
|
||||||
cacheTime = 0,
|
cacheTime = 0,
|
||||||
|
@ -575,7 +582,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
data class CoverImage(
|
data class CoverImage(
|
||||||
@JsonProperty("medium") val medium: String?,
|
@JsonProperty("medium") val medium: String?,
|
||||||
@JsonProperty("large") val large: String?
|
@JsonProperty("large") val large: String?,
|
||||||
|
@JsonProperty("extraLarge") val extraLarge: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Media(
|
data class Media(
|
||||||
|
@ -602,7 +610,29 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("score") val score: Int,
|
@JsonProperty("score") val score: Int,
|
||||||
@JsonProperty("private") val private: Boolean,
|
@JsonProperty("private") val private: Boolean,
|
||||||
@JsonProperty("media") val media: Media
|
@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(
|
data class Lists(
|
||||||
@JsonProperty("status") val status: String?,
|
@JsonProperty("status") val status: String?,
|
||||||
|
@ -617,40 +647,59 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
|
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getAnilistListCached(): Array<Lists>? {
|
private fun getAniListListCached(): Array<Lists>? {
|
||||||
return getKey(ANILIST_CACHED_LIST) as? 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 (getAuth() == null) return null
|
||||||
|
|
||||||
if (checkToken()) return null
|
if (checkToken()) return null
|
||||||
return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) {
|
return if (requireLibraryRefresh) {
|
||||||
val list = getFullAnilistList()?.data?.MediaListCollection?.lists?.toTypedArray()
|
val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray()
|
||||||
if (list != null) {
|
if (list != null) {
|
||||||
setKey(ANILIST_CACHED_LIST, list)
|
setKey(ANILIST_CACHED_LIST, list)
|
||||||
setKey(ANILIST_SHOULD_UPDATE_LIST, false)
|
|
||||||
}
|
}
|
||||||
list
|
list
|
||||||
} else {
|
} else {
|
||||||
getAnilistListCached()
|
getAniListListCached()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getFullAnilistList(): FullAnilistList? {
|
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
|
||||||
var userID: Int? = null
|
val list = getAniListAnimeListSmart()?.groupBy {
|
||||||
|
convertAniListStringToStatus(it.status ?: "").stringRes
|
||||||
|
}?.mapValues { group ->
|
||||||
|
group.value.map { it.entries.map { entry -> entry.toLibraryItem() } }.flatten()
|
||||||
|
} ?: emptyMap()
|
||||||
|
|
||||||
|
// To fill empty lists when AniList does not return them
|
||||||
|
val baseMap =
|
||||||
|
AniListStatusType.values().filter { it.value >= 0 }.associate {
|
||||||
|
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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! **/
|
/** WARNING ASSUMES ONE USER! **/
|
||||||
getKeys(ANILIST_USER_KEY)?.forEach { key ->
|
|
||||||
getKey<AniListUser>(key, null)?.let {
|
|
||||||
userID = it.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val fixedUserID = userID ?: return null
|
val userID = getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.id ?: return null
|
||||||
val mediaType = "ANIME"
|
val mediaType = "ANIME"
|
||||||
|
|
||||||
val query = """
|
val query = """
|
||||||
query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) {
|
query (${'$'}userID: Int = $userID, ${'$'}MEDIA: MediaType = $mediaType) {
|
||||||
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
|
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
|
||||||
lists {
|
lists {
|
||||||
status
|
status
|
||||||
|
@ -661,7 +710,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
startedAt { year month day }
|
startedAt { year month day }
|
||||||
updatedAt
|
updatedAt
|
||||||
progress
|
progress
|
||||||
score
|
score (format: POINT_100)
|
||||||
private
|
private
|
||||||
media
|
media
|
||||||
{
|
{
|
||||||
|
@ -677,7 +726,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
english
|
english
|
||||||
romaji
|
romaji
|
||||||
}
|
}
|
||||||
coverImage { medium }
|
coverImage { extraLarge large medium }
|
||||||
synonyms
|
synonyms
|
||||||
nextAiringEpisode {
|
nextAiringEpisode {
|
||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
|
|
|
@ -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<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.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<SyncAPI.LibraryItem>()
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders.providers
|
package com.lagradost.cloudstream3.syncproviders.providers
|
||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
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.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.ShowStatus
|
import com.lagradost.cloudstream3.ShowStatus
|
||||||
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
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.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
||||||
|
@ -31,13 +36,15 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override val redirectUrl = "mallogin"
|
override val redirectUrl = "mallogin"
|
||||||
override val idPrefix = "mal"
|
override val idPrefix = "mal"
|
||||||
override var mainUrl = "https://myanimelist.net"
|
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 icon = R.drawable.mal_logo
|
||||||
override val requiresLogin = false
|
override val requiresLogin = false
|
||||||
|
override val syncIdName = SyncIdName.MyAnimeList
|
||||||
|
override var requireLibraryRefresh = true
|
||||||
override val createAccountUrl = "$mainUrl/register.php"
|
override val createAccountUrl = "$mainUrl/register.php"
|
||||||
|
|
||||||
override fun logOut() {
|
override fun logOut() {
|
||||||
|
requireLibraryRefresh = true
|
||||||
removeAccountKeys()
|
removeAccountKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +97,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
fromIntToAnimeStatus(status.status),
|
fromIntToAnimeStatus(status.status),
|
||||||
status.score,
|
status.score,
|
||||||
status.watchedEpisodes
|
status.watchedEpisodes
|
||||||
)
|
).also {
|
||||||
|
requireLibraryRefresh = requireLibraryRefresh || it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MalAnime(
|
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_USER_KEY: String = "mal_user" // user data like profile
|
||||||
const val MAL_CACHED_LIST: String = "mal_cached_list"
|
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_UNIXTIME_KEY: String = "mal_unixtime" // When token expires
|
||||||
const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token
|
const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token
|
||||||
const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api
|
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 {
|
override suspend fun handleRedirect(url: String): Boolean {
|
||||||
|
@ -275,7 +319,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
switchToNewAccount()
|
switchToNewAccount()
|
||||||
storeToken(res)
|
storeToken(res)
|
||||||
val user = getMalUser()
|
val user = getMalUser()
|
||||||
setKey(MAL_SHOULD_UPDATE_LIST, true)
|
requireLibraryRefresh = true
|
||||||
return user != null
|
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_UNIXTIME_KEY, (token.expires_in + unixTime))
|
||||||
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
|
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
|
||||||
setKey(accountId, MAL_TOKEN_KEY, token.access_token)
|
setKey(accountId, MAL_TOKEN_KEY, token.access_token)
|
||||||
|
requireLibraryRefresh = true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
logError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +374,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
).text
|
).text
|
||||||
storeToken(res)
|
storeToken(res)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
logError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,7 +427,24 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
data class Data(
|
data class Data(
|
||||||
@JsonProperty("node") val node: Node,
|
@JsonProperty("node") val node: Node,
|
||||||
@JsonProperty("list_status") val list_status: ListStatus?,
|
@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(
|
data class Paging(
|
||||||
@JsonProperty("next") val next: String?
|
@JsonProperty("next") val next: String?
|
||||||
|
@ -413,18 +475,43 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return getKey(MAL_CACHED_LIST) as? Array<Data>
|
return getKey(MAL_CACHED_LIST) as? Array<Data>
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMalAnimeListSmart(): Array<Data>? {
|
private suspend fun getMalAnimeListSmart(): Array<Data>? {
|
||||||
if (getAuth() == null) return null
|
if (getAuth() == null) return null
|
||||||
return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) {
|
return if (requireLibraryRefresh) {
|
||||||
val list = getMalAnimeList()
|
val list = getMalAnimeList()
|
||||||
setKey(MAL_CACHED_LIST, list)
|
setKey(MAL_CACHED_LIST, list)
|
||||||
setKey(MAL_SHOULD_UPDATE_LIST, false)
|
|
||||||
list
|
list
|
||||||
} else {
|
} else {
|
||||||
getMalAnimeListCached()
|
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<SyncAPI.LibraryItem>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Data> {
|
private suspend fun getMalAnimeList(): Array<Data> {
|
||||||
checkMalToken()
|
checkMalToken()
|
||||||
var offset = 0
|
var offset = 0
|
||||||
|
@ -440,10 +527,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return fullList.toTypedArray()
|
return fullList.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertToStatus(string: String): MalStatusType {
|
|
||||||
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
|
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
|
||||||
val user = "@me"
|
val user = "@me"
|
||||||
val auth = getAuth() ?: return null
|
val auth = getAuth() ?: return null
|
||||||
|
@ -557,28 +640,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return user
|
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(
|
private suspend fun setScoreRequest(
|
||||||
id: Int,
|
id: Int,
|
||||||
status: MalStatusType? = null,
|
status: MalStatusType? = null,
|
||||||
|
|
|
@ -7,7 +7,8 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlin.math.abs
|
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(
|
override fun onFocusSearchFailed(
|
||||||
focused: View,
|
focused: View,
|
||||||
focusDirection: Int,
|
focusDirection: Int,
|
||||||
|
@ -34,7 +35,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManage
|
||||||
val pos = maxOf(0, getPosition(focused!!) - 2)
|
val pos = maxOf(0, getPosition(focused!!) - 2)
|
||||||
parent.scrollToPosition(pos)
|
parent.scrollToPosition(pos)
|
||||||
super.onRequestChildFocus(parent, state, child, focused)
|
super.onRequestChildFocus(parent, state, child, focused)
|
||||||
} catch (e: Exception){
|
} catch (e: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -569,7 +569,7 @@ class HomeFragment : Fragment() {
|
||||||
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
||||||
listHomepageItems.clear()
|
listHomepageItems.clear()
|
||||||
|
|
||||||
(home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(
|
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(
|
||||||
d.values.toMutableList(),
|
d.values.toMutableList(),
|
||||||
home_master_recycler
|
home_master_recycler
|
||||||
)
|
)
|
||||||
|
@ -621,7 +621,7 @@ class HomeFragment : Fragment() {
|
||||||
//home_loaded?.isVisible = false
|
//home_loaded?.isVisible = false
|
||||||
}
|
}
|
||||||
is Resource.Loading -> {
|
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_shimmer?.startShimmer()
|
||||||
home_loading?.isVisible = true
|
home_loading?.isVisible = true
|
||||||
home_loading_error?.isVisible = false
|
home_loading_error?.isVisible = false
|
||||||
|
|
|
@ -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<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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<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
|
||||||
|
)
|
||||||
|
} 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<Resource<List<SyncAPI.Page>>> = MutableLiveData(null)
|
||||||
|
val pages: LiveData<Resource<List<SyncAPI.Page>>> = _pages
|
||||||
|
|
||||||
|
private val _currentApiName: MutableLiveData<String> = MutableLiveData("")
|
||||||
|
val currentApiName: LiveData<String> = _currentApiName
|
||||||
|
|
||||||
|
private val availableSyncApis
|
||||||
|
get() = SyncApis.filter { it.hasAccount() }
|
||||||
|
|
||||||
|
var currentSyncApi = availableSyncApis.let { allApis ->
|
||||||
|
val lastSelection = getKey<String>(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<String>
|
||||||
|
get() = availableSyncApis.map { it.name }
|
||||||
|
|
||||||
|
var sortingMethods = emptyList<ListSorting>()
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<SyncAPI.LibraryItem>,
|
||||||
|
private val resView: AutofitRecyclerView,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<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], unbound.remove(position))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val unbound = mutableSetOf<Int>()
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -220,7 +220,7 @@ class QuickSearchFragment : Fragment() {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
it.value.let { data ->
|
it.value.let { data ->
|
||||||
(quick_search_autofit_results?.adapter as? SearchAdapter?)?.updateList(
|
(quick_search_autofit_results?.adapter as? SearchAdapter)?.updateList(
|
||||||
context?.filterSearchResultByFilmQuality(data) ?: data
|
context?.filterSearchResultByFilmQuality(data) ?: data
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,7 +277,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
||||||
private var downloadButton: EasyDownloadButton? = null
|
private var downloadButton: EasyDownloadButton? = null
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
updateUIListener = null
|
updateUIListener = null
|
||||||
(result_episodes?.adapter as EpisodeAdapter?)?.killAdapter()
|
(result_episodes?.adapter as? EpisodeAdapter)?.killAdapter()
|
||||||
downloadButton?.dispose()
|
downloadButton?.dispose()
|
||||||
|
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
@ -458,7 +458,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
||||||
temporary_no_focus?.requestFocus()
|
temporary_no_focus?.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
(result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value)
|
(result_episodes?.adapter as? EpisodeAdapter)?.updateList(episodes.value)
|
||||||
|
|
||||||
if (isTv && hasEpisodes) main {
|
if (isTv && hasEpisodes) main {
|
||||||
delay(500)
|
delay(500)
|
||||||
|
@ -687,7 +687,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
||||||
val newList = list.filter { it.isSynced && it.hasAccount }
|
val newList = list.filter { it.isSynced && it.hasAccount }
|
||||||
|
|
||||||
result_mini_sync?.isVisible = newList.isNotEmpty()
|
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
|
var currentSyncProgress = 0
|
||||||
|
@ -900,7 +900,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
||||||
|
|
||||||
|
|
||||||
result_cast_items?.isVisible = d.actors != null
|
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())
|
updateList(d.actors ?: emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -485,7 +485,7 @@ class ResultFragmentPhone : ResultFragment() {
|
||||||
|
|
||||||
result_recommendations?.post {
|
result_recommendations?.post {
|
||||||
rec?.let { list ->
|
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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ class ResultFragmentTv : ResultFragment() {
|
||||||
result_recommendations?.isGone = isInvalid
|
result_recommendations?.isGone = isInvalid
|
||||||
result_recommendations_holder?.isGone = isInvalid
|
result_recommendations_holder?.isGone = isInvalid
|
||||||
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
|
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())
|
?: emptyList())
|
||||||
|
|
||||||
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
|
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
|
||||||
|
|
|
@ -13,6 +13,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
import com.lagradost.cloudstream3.APIHolder.getId
|
import com.lagradost.cloudstream3.APIHolder.getId
|
||||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
|
@ -1443,12 +1444,18 @@ class ResultViewModel2 : ViewModel() {
|
||||||
|
|
||||||
val realRecommendations = ArrayList<SearchResponse>()
|
val realRecommendations = ArrayList<SearchResponse>()
|
||||||
// TODO: fix
|
// TODO: fix
|
||||||
//val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
|
val apiNames = apis.filter {
|
||||||
// meta.recommendations?.forEach { rec ->
|
it.name.contains("gogoanime", true) ||
|
||||||
// apiNames.forEach { name ->
|
it.name.contains("9anime", true)
|
||||||
// realRecommendations.add(rec.copy(apiName = name))
|
}.map {
|
||||||
// }
|
it.name
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
meta.recommendations?.forEach { rec ->
|
||||||
|
apiNames.forEach { name ->
|
||||||
|
realRecommendations.add(rec.copy(apiName = name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
recommendations = recommendations?.union(realRecommendations)?.toList()
|
recommendations = recommendations?.union(realRecommendations)?.toList()
|
||||||
?: realRecommendations
|
?: realRecommendations
|
||||||
|
@ -2143,7 +2150,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
val validUrlResource = safeApiCall {
|
val validUrlResource = safeApiCall {
|
||||||
SyncRedirector.redirect(
|
SyncRedirector.redirect(
|
||||||
url,
|
url,
|
||||||
api.mainUrl
|
api
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// TODO: fix
|
// TODO: fix
|
||||||
|
|
|
@ -10,12 +10,16 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.SearchResponse
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
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.IsBottomLayout
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import kotlinx.android.synthetic.main.search_result_compact.view.*
|
import kotlinx.android.synthetic.main.search_result_compact.view.*
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/** Click */
|
||||||
const val SEARCH_ACTION_LOAD = 0
|
const val SEARCH_ACTION_LOAD = 0
|
||||||
|
|
||||||
|
/** Long press */
|
||||||
const val SEARCH_ACTION_SHOW_METADATA = 1
|
const val SEARCH_ACTION_SHOW_METADATA = 1
|
||||||
const val SEARCH_ACTION_PLAY_FILE = 2
|
const val SEARCH_ACTION_PLAY_FILE = 2
|
||||||
const val SEARCH_ACTION_FOCUSED = 4
|
const val SEARCH_ACTION_FOCUSED = 4
|
||||||
|
|
|
@ -420,7 +420,7 @@ class SearchFragment : Fragment() {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
it.value.let { data ->
|
it.value.let { data ->
|
||||||
if (data.isNotEmpty()) {
|
if (data.isNotEmpty()) {
|
||||||
(search_autofit_results?.adapter as SearchAdapter?)?.updateList(data)
|
(search_autofit_results?.adapter as? SearchAdapter)?.updateList(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
searchExitIcon.alpha = 1f
|
searchExitIcon.alpha = 1f
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package com.lagradost.cloudstream3.ui.search
|
package com.lagradost.cloudstream3.ui.search
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.palette.graphics.Palette
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||||
|
@ -41,6 +43,7 @@ object SearchResultBuilder {
|
||||||
nextFocusBehavior: Boolean? = null,
|
nextFocusBehavior: Boolean? = null,
|
||||||
nextFocusUp: Int? = null,
|
nextFocusUp: Int? = null,
|
||||||
nextFocusDown: Int? = null,
|
nextFocusDown: Int? = null,
|
||||||
|
colorCallback : ((Palette) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val cardView: ImageView = itemView.imageView
|
val cardView: ImageView = itemView.imageView
|
||||||
val cardText: TextView? = itemView.imageText
|
val cardText: TextView? = itemView.imageText
|
||||||
|
@ -100,7 +103,7 @@ object SearchResultBuilder {
|
||||||
cardText?.isVisible = showTitle
|
cardText?.isVisible = showTitle
|
||||||
cardView.isVisible = true
|
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)
|
cardView.setImageResource(R.drawable.default_cover)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -143,7 +143,7 @@ class PluginsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) ->
|
observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) ->
|
||||||
(plugin_recycler_view?.adapter as? PluginAdapter?)?.updateList(list)
|
(plugin_recycler_view?.adapter as? PluginAdapter)?.updateList(list)
|
||||||
|
|
||||||
if (scrollToTop)
|
if (scrollToTop)
|
||||||
plugin_recycler_view?.scrollToPosition(0)
|
plugin_recycler_view?.scrollToPosition(0)
|
||||||
|
|
|
@ -28,10 +28,12 @@ import androidx.core.text.toSpanned
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.tvprovider.media.tv.*
|
import androidx.tvprovider.media.tv.*
|
||||||
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
|
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.google.android.gms.cast.framework.CastContext
|
import com.google.android.gms.cast.framework.CastContext
|
||||||
import com.google.android.gms.cast.framework.CastState
|
import com.google.android.gms.cast.framework.CastState
|
||||||
|
@ -65,6 +67,7 @@ import okhttp3.Cache
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
object AppUtils {
|
object AppUtils {
|
||||||
fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) {
|
fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) {
|
||||||
|
@ -164,6 +167,18 @@ object AppUtils {
|
||||||
return builder.build()
|
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")
|
@SuppressLint("RestrictedApi")
|
||||||
fun getAllWatchNextPrograms(context: Context): Set<Long> {
|
fun getAllWatchNextPrograms(context: Context): Set<Long> {
|
||||||
val COLUMN_WATCH_NEXT_ID_INDEX = 0
|
val COLUMN_WATCH_NEXT_ID_INDEX = 0
|
||||||
|
@ -329,6 +344,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) {
|
fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
val context = this
|
val context = this
|
||||||
|
|
|
@ -18,13 +18,11 @@ import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY
|
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY
|
||||||
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY_LOCAL
|
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_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_TOKEN_KEY
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_UNIXTIME_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.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_CACHED_LIST
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_REFRESH_TOKEN_KEY
|
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_TOKEN_KEY
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY
|
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_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
|
// When sharing backup we do not want to transfer what is essentially the password
|
||||||
ANILIST_TOKEN_KEY,
|
ANILIST_TOKEN_KEY,
|
||||||
ANILIST_CACHED_LIST,
|
ANILIST_CACHED_LIST,
|
||||||
ANILIST_SHOULD_UPDATE_LIST,
|
|
||||||
ANILIST_UNIXTIME_KEY,
|
ANILIST_UNIXTIME_KEY,
|
||||||
ANILIST_USER_KEY,
|
ANILIST_USER_KEY,
|
||||||
MAL_TOKEN_KEY,
|
MAL_TOKEN_KEY,
|
||||||
MAL_REFRESH_TOKEN_KEY,
|
MAL_REFRESH_TOKEN_KEY,
|
||||||
MAL_SHOULD_UPDATE_LIST,
|
|
||||||
MAL_CACHED_LIST,
|
MAL_CACHED_LIST,
|
||||||
MAL_UNIXTIME_KEY,
|
MAL_UNIXTIME_KEY,
|
||||||
MAL_USER_KEY,
|
MAL_USER_KEY,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.utils
|
package com.lagradost.cloudstream3.utils
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
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.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
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.SearchQuality
|
||||||
import com.lagradost.cloudstream3.SearchResponse
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
import com.lagradost.cloudstream3.TvType
|
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.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.result.VideoWatchState
|
import com.lagradost.cloudstream3.ui.result.VideoWatchState
|
||||||
|
|
||||||
|
@ -51,7 +54,20 @@ object DataStoreHelper {
|
||||||
@JsonProperty("year") val year: Int?,
|
@JsonProperty("year") val year: Int?,
|
||||||
@JsonProperty("quality") override var quality: SearchQuality? = null,
|
@JsonProperty("quality") override var quality: SearchQuality? = null,
|
||||||
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = 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(
|
data class ResumeWatchingResult(
|
||||||
@JsonProperty("name") override val name: String,
|
@JsonProperty("name") override val name: String,
|
||||||
|
@ -71,6 +87,9 @@ object DataStoreHelper {
|
||||||
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
||||||
) : SearchResponse
|
) : SearchResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A datastore wide account for future implementations of a multiple account system
|
||||||
|
**/
|
||||||
private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
|
private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
|
||||||
|
|
||||||
fun getAllWatchStateIds(): List<Int>? {
|
fun getAllWatchStateIds(): List<Int>? {
|
||||||
|
@ -177,6 +196,7 @@ object DataStoreHelper {
|
||||||
fun setBookmarkedData(id: Int?, data: BookmarkedData) {
|
fun setBookmarkedData(id: Int?, data: BookmarkedData) {
|
||||||
if (id == null) return
|
if (id == null) return
|
||||||
setKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString(), data)
|
setKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString(), data)
|
||||||
|
AccountManager.localListApi.requireLibraryRefresh = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBookmarkedData(id: Int?): BookmarkedData? {
|
fun getBookmarkedData(id: Int?): BookmarkedData? {
|
||||||
|
|
|
@ -4,6 +4,7 @@ package com.lagradost.cloudstream3.utils
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
//import com.lagradost.cloudstream3.animeproviders.AniflixProvider
|
//import com.lagradost.cloudstream3.animeproviders.AniflixProvider
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
@ -78,17 +79,21 @@ object SyncUtil {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUrlsFromId(id: String, type: String = "anilist") : List<String> {
|
suspend fun getUrlsFromId(id: String, type: String = "anilist"): List<String> {
|
||||||
return arrayListOf()
|
val url =
|
||||||
// val url =
|
"https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json"
|
||||||
// "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 response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed<SyncPage>()
|
val pages = response.pages ?: return emptyList()
|
||||||
// val pages = response.pages ?: return emptyList()
|
val current =
|
||||||
// val current = pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values).mapNotNull { it.url }.toMutableList()
|
pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values)
|
||||||
// if(type == "anilist") { // TODO MAKE BETTER
|
.mapNotNull { it.url }.toMutableList()
|
||||||
// current.add("${AniflixProvider().mainUrl}/anime/$id")
|
|
||||||
// }
|
if (type == "anilist") { // TODO MAKE BETTER
|
||||||
// return current
|
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
|
||||||
|
current.add("${it.mainUrl}/anime/$id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SyncPage(
|
data class SyncPage(
|
||||||
|
|
|
@ -9,7 +9,9 @@ import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
@ -28,15 +30,21 @@ import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.alpha
|
import androidx.core.graphics.alpha
|
||||||
import androidx.core.graphics.blue
|
import androidx.core.graphics.blue
|
||||||
|
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||||
import androidx.core.graphics.green
|
import androidx.core.graphics.green
|
||||||
import androidx.core.graphics.red
|
import androidx.core.graphics.red
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import androidx.palette.graphics.Palette
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
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.model.GlideUrl
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
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.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||||
|
@ -105,7 +113,7 @@ object UIHelper {
|
||||||
listView.requestLayout()
|
listView.requestLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Activity?.getSpanCount(): Int? {
|
fun Context?.getSpanCount(): Int? {
|
||||||
val compactView = false
|
val compactView = false
|
||||||
val spanCountLandscape = if (compactView) 2 else 6
|
val spanCountLandscape = if (compactView) 2 else 6
|
||||||
val spanCountPortrait = if (compactView) 1 else 3
|
val spanCountPortrait = if (compactView) 1 else 3
|
||||||
|
@ -158,12 +166,27 @@ object UIHelper {
|
||||||
return color
|
return color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var createPaletteAsyncCache: HashMap<String, Palette> = 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(
|
fun ImageView?.setImage(
|
||||||
url: String?,
|
url: String?,
|
||||||
headers: Map<String, String>? = null,
|
headers: Map<String, String>? = null,
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
errorImageDrawable: Int? = null,
|
errorImageDrawable: Int? = null,
|
||||||
fadeIn: Boolean = true
|
fadeIn: Boolean = true,
|
||||||
|
colorCallback: ((Palette) -> Unit)? = null
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (this == null || url.isNullOrBlank()) return false
|
if (this == null || url.isNullOrBlank()) return false
|
||||||
|
|
||||||
|
@ -177,6 +200,33 @@ object UIHelper {
|
||||||
else req
|
else req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (colorCallback != null) {
|
||||||
|
builder.listener(object : RequestListener<Drawable> {
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: Drawable?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<Drawable>?,
|
||||||
|
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<Drawable>?,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
val res = if (errorImageDrawable != null)
|
val res = if (errorImageDrawable != null)
|
||||||
builder.error(errorImageDrawable).into(this)
|
builder.error(errorImageDrawable).into(this)
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<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_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"/>
|
<item android:color="?attr/grayTextColor" android:state_checked="false"/>
|
||||||
</selector>
|
</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: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"/>
|
<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>
|
</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>
|
|
@ -35,9 +35,9 @@
|
||||||
-->
|
-->
|
||||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
android:id="@+id/nav_view"
|
android:id="@+id/nav_view"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="70dp"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
app:labelVisibilityMode="labeled"
|
app:labelVisibilityMode="unlabeled"
|
||||||
android:background="?attr/primaryGrayBackground"
|
android:background="?attr/primaryGrayBackground"
|
||||||
|
|
||||||
app:itemIconTint="@color/item_select_color"
|
app:itemIconTint="@color/item_select_color"
|
||||||
|
|
177
app/src/main/res/layout/fragment_library.xml
Normal file
177
app/src/main/res/layout/fragment_library.xml
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
<?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">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_list_textview"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_margin="30dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<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:id="@+id/search_status_bar_padding"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_scrollFlags="scroll|enterAlways">
|
||||||
|
|
||||||
|
<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="45dp"
|
||||||
|
android:layout_height="45dp"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/change_providers_img_des"
|
||||||
|
android:nextFocusLeft="@id/main_search"
|
||||||
|
android:nextFocusRight="@id/main_search"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:src="@drawable/ic_baseline_filter_list_24"
|
||||||
|
app:tint="?attr/textColor" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
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"
|
||||||
|
tools:listitem="@layout/library_viewpager_page" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/library_loading_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/primaryBlackBackground"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<com.facebook.shimmer.ShimmerFrameLayout
|
||||||
|
android:id="@+id/library_loading_shimmer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_margin="2dp"
|
||||||
|
app:shimmer_auto_start="true"
|
||||||
|
app:shimmer_base_alpha="0.2"
|
||||||
|
app:shimmer_duration="@integer/loading_time"
|
||||||
|
app:shimmer_highlight_alpha="0.3">
|
||||||
|
|
||||||
|
<GridView
|
||||||
|
android:id="@+id/gridview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:gravity="center"
|
||||||
|
android:horizontalSpacing="10dp"
|
||||||
|
android:numColumns="3"
|
||||||
|
android:paddingBottom="120dp"
|
||||||
|
android:verticalSpacing="10dp"
|
||||||
|
tools:listitem="@layout/loading_poster_dynamic" />
|
||||||
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<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="@string/sort"
|
||||||
|
android:textColor="?attr/textColor"
|
||||||
|
app:icon="@drawable/ic_baseline_sort_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<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: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" />
|
||||||
|
|
||||||
|
</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" />
|
||||||
|
|
37
app/src/main/res/layout/loading_poster_dynamic.xml
Normal file
37
app/src/main/res/layout/loading_poster_dynamic.xml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/card_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:background="@color/grayShimmer"
|
||||||
|
app:cardCornerRadius="@dimen/loading_radius"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.414"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/loading_line_short_center"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="15dp"
|
||||||
|
android:layout_marginHorizontal="20dp"
|
||||||
|
android:layout_marginVertical="10dp" />
|
||||||
|
</LinearLayout>
|
|
@ -5,26 +5,32 @@
|
||||||
android:id="@+id/search_result_root"
|
android:id="@+id/search_result_root"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:foreground="@drawable/outline_drawable"
|
android:foreground="@drawable/outline_drawable"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:id="@+id/background_card"
|
android:id="@+id/background_card"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="2dp"
|
android:layout_margin="2dp"
|
||||||
android:layout_marginBottom="2dp"
|
android:layout_marginBottom="2dp"
|
||||||
android:elevation="10dp"
|
android:elevation="10dp"
|
||||||
app:cardBackgroundColor="?attr/primaryGrayBackground"
|
app:cardBackgroundColor="?attr/primaryGrayBackground"
|
||||||
app:cardCornerRadius="@dimen/rounded_image_radius">
|
app:cardCornerRadius="@dimen/rounded_image_radius"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.414"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageView"
|
android:id="@+id/imageView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:contentDescription="@string/search_poster_img_des"
|
android:contentDescription="@string/search_poster_img_des"
|
||||||
android:duplicateParentState="true"
|
android:duplicateParentState="true"
|
||||||
|
@ -32,7 +38,9 @@
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
tools:src="@drawable/example_poster" />
|
tools:src="@drawable/example_poster" />
|
||||||
|
|
||||||
<TextView android:id="@+id/text_quality" style="@style/TypeButton" />
|
<TextView
|
||||||
|
android:id="@+id/text_quality"
|
||||||
|
style="@style/TypeButton" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -50,6 +58,30 @@
|
||||||
style="@style/SubButton"
|
style="@style/SubButton"
|
||||||
android:layout_gravity="end" />
|
android:layout_gravity="end" />
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/text_rating_holder"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_margin="2dp"
|
||||||
|
android:backgroundTint="@color/ratingColorBg"
|
||||||
|
android:elevation="0dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:cardCornerRadius="@dimen/rounded_image_radius"
|
||||||
|
app:cardElevation="0dp"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_rating"
|
||||||
|
style="@style/SearchBox"
|
||||||
|
android:layout_margin="0dp"
|
||||||
|
android:minWidth="40dp"
|
||||||
|
android:textColor="@color/ratingColor"
|
||||||
|
|
||||||
|
tools:text="★ 7.7" />
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_flag"
|
android:id="@+id/text_flag"
|
||||||
style="@style/SearchBox"
|
style="@style/SearchBox"
|
||||||
|
@ -60,7 +92,22 @@
|
||||||
tools:text="🇸🇪"
|
tools:text="🇸🇪"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<androidx.core.widget.ContentLoadingProgressBar
|
||||||
|
android:id="@+id/watchProgress"
|
||||||
|
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="5dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:layout_marginBottom="-1.5dp"
|
||||||
|
android:progressBackgroundTint="?attr/colorPrimary"
|
||||||
|
android:progressTint="?attr/colorPrimary"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:progress="50"
|
||||||
|
tools:visibility="visible" />
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/imageText"
|
android:id="@+id/imageText"
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_home"
|
android:id="@+id/navigation_home"
|
||||||
android:icon="@drawable/home_alt"
|
android:icon="@drawable/home_alt"
|
||||||
android:title="@string/title_home"/>
|
android:title="@string/title_home" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_search"
|
android:id="@+id/navigation_search"
|
||||||
android:icon="@drawable/search_icon"
|
android:icon="@drawable/search_icon"
|
||||||
android:title="@string/title_search"/>
|
android:title="@string/title_search" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/navigation_library"
|
||||||
|
android:icon="@drawable/ic_outline_account_circle_24"
|
||||||
|
android:title="@string/library" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_downloads"
|
android:id="@+id/navigation_downloads"
|
||||||
android:icon="@drawable/netflix_download"
|
android:icon="@drawable/netflix_download"
|
||||||
android:title="@string/title_downloads"/>
|
android:title="@string/title_downloads" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_settings"
|
android:id="@+id/navigation_settings"
|
||||||
android:icon="@drawable/settings_alt"
|
android:icon="@drawable/ic_outline_settings_24"
|
||||||
android:title="@string/title_settings"/>
|
android:title="@string/title_settings" />
|
||||||
</menu>
|
</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:popEnterAnim="@anim/enter_anim"
|
||||||
app:popExitAnim="@anim/exit_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
|
<fragment
|
||||||
android:id="@+id/navigation_settings_general"
|
android:id="@+id/navigation_settings_general"
|
||||||
android:name="com.lagradost.cloudstream3.ui.settings.SettingsGeneral"
|
android:name="com.lagradost.cloudstream3.ui.settings.SettingsGeneral"
|
||||||
|
|
|
@ -36,6 +36,8 @@
|
||||||
<color name="subColorBg">#F53B66</color>
|
<color name="subColorBg">#F53B66</color>
|
||||||
<color name="typeColorText">#BEC8FF</color>
|
<color name="typeColorText">#BEC8FF</color>
|
||||||
<color name="typeColorBg">?attr/colorPrimaryDark</color>
|
<color name="typeColorBg">?attr/colorPrimaryDark</color>
|
||||||
|
<color name="ratingColor">#4C3115</color>
|
||||||
|
<color name="ratingColorBg">#FFA662</color>
|
||||||
|
|
||||||
<color name="adultColor">#FF6F63</color> <!-- same as sub color -->
|
<color name="adultColor">#FF6F63</color> <!-- same as sub color -->
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,7 @@
|
||||||
<string name="result_tags">Genres</string>
|
<string name="result_tags">Genres</string>
|
||||||
<string name="result_share">Share</string>
|
<string name="result_share">Share</string>
|
||||||
<string name="result_open_in_browser">Open In Browser</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="skip_loading">Skip Loading</string>
|
||||||
<string name="loading">Loading…</string>
|
<string name="loading">Loading…</string>
|
||||||
<string name="type_watching">Watching</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">Storage permissions missing. Please try again.</string>
|
||||||
<string name="backup_failed_error_format">Error backing up %s</string>
|
<string name="backup_failed_error_format">Error backing up %s</string>
|
||||||
<string name="search">Search</string>
|
<string name="search">Search</string>
|
||||||
|
<string name="library">Library</string>
|
||||||
<string name="category_account">Accounts</string>
|
<string name="category_account">Accounts</string>
|
||||||
<string name="category_updates">Updates and backup</string>
|
<string name="category_updates">Updates and backup</string>
|
||||||
<string name="settings_info">Info</string>
|
<string name="settings_info">Info</string>
|
||||||
|
@ -616,5 +618,16 @@
|
||||||
<string name="apk_installer_legacy">Legacy</string>
|
<string name="apk_installer_legacy">Legacy</string>
|
||||||
<string name="apk_installer_package_installer">PackageInstaller</string>
|
<string name="apk_installer_package_installer">PackageInstaller</string>
|
||||||
<string name="delayed_update_notice">App will be updated upon exit</string>
|
<string name="delayed_update_notice">App will be updated upon exit</string>
|
||||||
|
<string name="sort_by">Sort by</string>
|
||||||
|
<string name="sort">Sort</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>
|
||||||
|
<string name="empty_library_no_accounts_message">Looks like your library is empty :(\nLogin to a library account or add shows to your local library</string>
|
||||||
|
<string name="empty_library_logged_in_message">Looks like this list is empty, try switching to another one</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -90,11 +90,13 @@
|
||||||
<item name="android:fontFamily">@font/google_sans</item>
|
<item name="android:fontFamily">@font/google_sans</item>
|
||||||
<item name="chipMinTouchTargetSize">0dp</item>
|
<item name="chipMinTouchTargetSize">0dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="ChipFilledSemiTransparent" parent="@style/ChipFilled">
|
<style name="ChipFilledSemiTransparent" parent="@style/ChipFilled">
|
||||||
<item name="chipBackgroundColor">@color/transparent</item>
|
<item name="chipBackgroundColor">@color/transparent</item>
|
||||||
<item name="chipSurfaceColor">@color/semiWhite</item>
|
<item name="chipSurfaceColor">@color/semiWhite</item>
|
||||||
<item name="backgroundColor">@color/transparent</item>
|
<item name="backgroundColor">@color/transparent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="ChipParent">
|
<style name="ChipParent">
|
||||||
<item name="chipSpacingVertical">5dp</item>
|
<item name="chipSpacingVertical">5dp</item>
|
||||||
<item name="chipSpacingHorizontal">5dp</item>
|
<item name="chipSpacingHorizontal">5dp</item>
|
||||||
|
@ -123,6 +125,14 @@
|
||||||
<item name="android:textColor">@color/subColorText</item>
|
<item name="android:textColor">@color/subColorText</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="RatingButton" parent="@style/SearchBox">
|
||||||
|
<item name="android:minWidth">30dp</item>
|
||||||
|
<item name="android:background">@drawable/rating_bg_color</item>
|
||||||
|
<item name="drawableTint">@color/ratingColor</item>
|
||||||
|
<item name="android:textColor">@color/ratingColor</item>
|
||||||
|
<item name="drawableStartCompat">@drawable/ic_baseline_star_24</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="TypeButton" parent="@style/SearchBox">
|
<style name="TypeButton" parent="@style/SearchBox">
|
||||||
<item name="android:background">@drawable/type_bg_color</item>
|
<item name="android:background">@drawable/type_bg_color</item>
|
||||||
<item name="android:text">@string/quality_hd</item>
|
<item name="android:text">@string/quality_hd</item>
|
||||||
|
@ -375,6 +385,11 @@
|
||||||
<item name="textAllCaps">false</item>
|
<item name="textAllCaps">false</item>
|
||||||
</style>
|
</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">
|
<style name="AppTextViewStyle" parent="android:Widget.TextView">
|
||||||
<item name="android:fontFamily">@font/google_sans</item>
|
<item name="android:fontFamily">@font/google_sans</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue