Compare commits

...

33 commits

Author SHA1 Message Date
reduplicated
7a1cfc2f2b mini fix to text in popup 2023-02-02 18:30:48 +01:00
Blatzar
a8bcf2dcb2 Merge remote-tracking branch 'origin/anilist' into anilist 2023-01-28 23:36:30 +01:00
Blatzar
df95931d0a future proofed SyncID 2023-01-28 23:36:24 +01:00
LagradOst
46732ea917
Merge branch 'master' into anilist 2023-01-28 22:21:44 +00:00
reduplicated
cd166cb0d3 mini fix 2023-01-28 23:08:55 +01:00
Blatzar
cbeeff5cca fix library statusbar 2023-01-28 22:49:58 +01:00
Blatzar
e7732cc15f Smoother fast scrolling in library & no labels 2023-01-28 14:57:06 +01:00
Blatzar
e875d520de Fixed loading padding 2023-01-28 01:11:13 +01:00
Blatzar
ef7e0ecf0d Fixed rotation and loading delay 2023-01-28 00:54:47 +01:00
Blatzar
7512dbcbf8 Library loading fixes 2023-01-27 22:26:24 +01:00
Blatzar
1f67644290 Fix crash and sorting methods in library 2023-01-27 14:54:24 +01:00
Blatzar
0aca996bc0 Better notice when not logged in in library 2023-01-26 18:09:22 +01:00
Blatzar
26320bb535 Reworked backend to make list titles use UiText 2023-01-25 19:20:53 +01:00
reduplicated
9bc90438ec colors go brrr 2023-01-25 16:54:57 +01:00
Blatzar
dd293e9564 Added visible loading in library and fix reloading on sync 2023-01-25 12:54:51 +01:00
Blatzar
f6c609cfdf Fix embarrassing crash when using different languages in library 2023-01-25 00:55:01 +01:00
Blatzar
4a0989c9c5 fixed buggy page transform in library 2023-01-24 23:04:03 +01:00
Blatzar
847c70a127 Add search option in library 2023-01-24 22:38:06 +01:00
Blatzar
20ada8da0d Fix library bug and remove it from TV 2023-01-24 22:31:31 +01:00
Blatzar
daaab9d35b More library fixes 2023-01-24 20:27:22 +01:00
Blatzar
c4afb5e673 More library fixes 2023-01-22 23:25:29 +01:00
Blatzar
0dab9959e1 Fixes for breakages in the latest merge 2023-01-22 22:28:51 +01:00
LagradOst
e945600940
Merge pull request #325 from recloudstream/library
Bump anilist base to latest
2023-01-22 18:18:14 +00:00
LagradOst
9edb7f8999
Merge branch 'anilist' into library 2023-01-22 18:17:22 +00:00
Blatzar
7053d77ca1 Moved stuff to R.strings and fixed reloading the page 2023-01-22 19:09:37 +01:00
Blatzar
30dbd3920e Fixed "open with" in library 2023-01-22 18:54:16 +01:00
Blatzar
019e9a0c4f More work on local list support 2023-01-21 14:21:36 +01:00
Blatzar
7cd6dd3fe6 WIP local list support 2023-01-21 13:14:17 +01:00
Blatzar
8cdc31ffcd WIP adding open with specified extension 2022-12-28 20:26:59 +01:00
Blatzar
2a2a0a26e7 Add list switcher 2022-12-18 19:22:35 +01:00
Blatzar
67a1c447ae backend fixes for opening from sync urls 2022-12-05 20:49:45 +01:00
Blatzar
05dc032df6 library fixes 2022-11-20 17:05:11 +01:00
Blatzar
67fe6730e0 library work in progress 2022-11-19 20:44:12 +01:00
52 changed files with 1857 additions and 234 deletions

View file

@ -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) {

View file

@ -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*/

View file

@ -63,10 +63,7 @@ import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.home.HomeViewModel import com.lagradost.cloudstream3.ui.home.HomeViewModel
import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.*
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
@ -388,6 +385,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 +436,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
@ -791,7 +794,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
resultview_preview_meta_duration.setText(d.durationText) resultview_preview_meta_duration.setText(d.durationText)
resultview_preview_meta_rating.setText(d.ratingText) resultview_preview_meta_rating.setText(d.ratingText)
resultview_preview_description?.setText(d.plotText) resultview_preview_description?.setTextHtml(d.plotText)
resultview_preview_poster?.setImage( resultview_preview_poster?.setImage(
d.posterImage ?: d.posterBackgroundImage d.posterImage ?: d.posterBackgroundImage
) )

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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
} }

View file

@ -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)
}
} }

View file

@ -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

View file

@ -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
}
}

View file

@ -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,

View file

@ -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
} }
} }

View file

@ -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

View file

@ -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()
}
}

View file

@ -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
)
}
}

View file

@ -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))
}
}
}
}

View file

@ -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)
}
}

View file

@ -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"
}
}
}
}

View file

@ -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
}
}

View file

@ -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
) )
} }

View file

@ -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())
} }

View file

@ -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 })
} }
} }
} }

View file

@ -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 ->

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
} }

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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? {

View file

@ -250,17 +250,6 @@ object SingleSelectionHelper {
) )
} }
fun showBottomDialog(
items: List<String>,
selectedIndex: Int,
name: String,
showApply: Boolean,
dismissCallback: () -> Unit,
callback: (Int) -> Unit,
) {
}
/** Only for a low amount of items */ /** Only for a low amount of items */
fun Activity?.showBottomDialog( fun Activity?.showBottomDialog(
items: List<String>, items: List<String>,

View file

@ -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(

View file

@ -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

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View file

@ -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"

View 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>

View 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" />

View 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>

View file

@ -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"

View file

@ -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>

View 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>

View file

@ -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"

View file

@ -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 -->

View file

@ -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>

View file

@ -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>