More work on local list support

This commit is contained in:
Blatzar 2023-01-21 14:21:36 +01:00
parent 7cd6dd3fe6
commit 019e9a0c4f
11 changed files with 143 additions and 109 deletions

View File

@ -1,7 +1,8 @@
package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.ui.library.LibraryItem
import com.lagradost.cloudstream3.ui.library.ListSorting
import me.xdrop.fuzzywuzzy.FuzzySearch
enum class SyncIdName {
Anilist,
@ -37,9 +38,9 @@ interface SyncAPI : OAuth2API {
suspend fun search(name: String): List<SyncSearchResult>?
suspend fun getPersonalLibrary(): List<LibraryItem>?
suspend fun getPersonalLibrary(): LibraryMetadata?
fun getIdFromUrl(url : String) : String
fun getIdFromUrl(url: String): String
data class SyncSearchResult(
override val name: String,
@ -59,7 +60,7 @@ interface SyncAPI : OAuth2API {
val score: Int?,
val watchedEpisodes: Int?,
var isFavorite: Boolean? = null,
var maxEpisodes : Int? = null,
var maxEpisodes: Int? = null,
)
data class SyncResult(
@ -80,9 +81,9 @@ interface SyncAPI : OAuth2API {
var genres: List<String>? = null,
var synonyms: List<String>? = null,
var trailers: List<String>? = null,
var isAdult : Boolean? = null,
var isAdult: Boolean? = null,
var posterUrl: String? = null,
var backgroundPosterUrl : String? = null,
var backgroundPosterUrl: String? = null,
/** In unixtime */
var startDate: Long? = null,
@ -93,4 +94,53 @@ interface SyncAPI : OAuth2API {
var prevSeason: SyncSearchResult? = null,
var actors: List<ActorData>? = null,
)
data class Page(
val title: String, var items: List<LibraryItem>
) {
fun sort(method: ListSorting?, query: String? = null) {
items = when (method) {
ListSorting.Query ->
if (query != null) {
items.sortedBy {
-FuzzySearch.partialRatio(
query.lowercase(), it.name.lowercase()
)
}
} else items
ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating ?: 0) }
ListSorting.RatingLow -> items.sortedBy { (it.personalRating ?: 0) }
ListSorting.AlphabeticalA -> items.sortedBy { it.name }
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
else -> items
}
}
}
data class LibraryMetadata(
/** List of all available pages, useful to show empty pages
* if the user has no entry on that page */
val allListNames: List<String>,
/** Not necessarily sorted list of all library items, will be grouped by listName */
val allLibraryItems: List<LibraryItem>
)
data class LibraryItem(
override val name: String,
override val url: String,
/** Unique unchanging string used for data storage */
val syncId: String,
val listName: String,
val episodesCompleted: Int?,
val episodesTotal: Int?,
/** Out of 100 */
val personalRating: Int?,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override var posterHeaders: Map<String, String>?,
override var quality: SearchQuality?,
override var id: Int? = null,
) : SearchResponse
}

View File

@ -4,7 +4,6 @@ import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.ui.library.LibraryItem
class SyncRepo(private val repo: SyncAPI) {
val idPrefix = repo.idPrefix
@ -30,7 +29,7 @@ class SyncRepo(private val repo: SyncAPI) {
return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() }
}
suspend fun getPersonalLibrary(): Resource<List<LibraryItem>> {
suspend fun getPersonalLibrary(): Resource<SyncAPI.LibraryMetadata> {
return safeApiCall { repo.getPersonalLibrary() ?: throw ErrorLoadingException() }
}

View File

@ -11,7 +11,6 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.LibraryItem
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.toJson
@ -598,8 +597,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("private") val private: Boolean,
@JsonProperty("media") val media: Media
) {
fun toLibraryItem(listName: String?): LibraryItem? {
return LibraryItem(
fun toLibraryItem(listName: String?): SyncAPI.LibraryItem? {
return SyncAPI.LibraryItem(
// English title first
this.media.title.english ?: this.media.title.romaji
?: this.media.synonyms.firstOrNull()
@ -612,7 +611,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
this.score,
"AniList",
TvType.Anime,
this.media.coverImage.extraLarge ?: this.media.coverImage.large ?: this.media.coverImage.medium,
this.media.coverImage.extraLarge ?: this.media.coverImage.large
?: this.media.coverImage.medium,
null,
null,
null
@ -653,14 +653,17 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
}
override suspend fun getPersonalLibrary(): List<LibraryItem>? {
return getAniListAnimeListSmart()?.map {
it.entries.mapNotNull { entry ->
entry.toLibraryItem(
entry.status ?: it.status
)
}
}?.flatten()
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
return SyncAPI.LibraryMetadata(
emptyList(),
getAniListAnimeListSmart()?.map {
it.entries.mapNotNull { entry ->
entry.toLibraryItem(
entry.status ?: it.status
)
}
}?.flatten() ?: emptyList()
)
}
private suspend fun getFullAniListList(): FullAnilistList? {

View File

@ -1,10 +1,12 @@
package com.lagradost.cloudstream3.syncproviders.providers
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.LibraryItem
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
@ -17,8 +19,12 @@ class LocalList : SyncAPI {
override val createAccountUrl: Nothing? = null
override val idPrefix = "local"
override fun loginInfo(): AuthAPI.LoginInfo? {
return null
override fun loginInfo(): AuthAPI.LoginInfo {
return AuthAPI.LoginInfo(
null,
null,
0
)
}
override fun logOut() {
@ -52,18 +58,29 @@ class LocalList : SyncAPI {
return null
}
override suspend fun getPersonalLibrary(): List<LibraryItem> {
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? {
val watchStatusIds = ioWork {
getAllWatchStateIds()?.map { id ->
Pair(id, getResultWatchState(id))
}
}?.distinctBy { it.first } ?: return emptyList()
}?.distinctBy { it.first } ?: return null
return ioWork {
val list = ioWork {
watchStatusIds.mapNotNull {
getBookmarkedData(it.first)?.toLibraryItem(it.second)
}
}
return SyncAPI.LibraryMetadata(
WatchType.values().mapNotNull {
// None is not something to display
if (it == WatchType.NONE) return@mapNotNull null
// Dirty hack for context!
txt(it.stringRes).asStringNull(AcraApplication.context)
},
list
)
}
override fun getIdFromUrl(url: String): String {

View File

@ -14,7 +14,6 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.LibraryItem
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
@ -386,8 +385,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("node") val node: Node,
@JsonProperty("list_status") val list_status: ListStatus?,
) {
fun toLibraryItem(): LibraryItem {
return LibraryItem(
fun toLibraryItem(): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
this.node.title,
"https://myanimelist.net/anime/${this.node.id}/",
this.node.id.toString(),
@ -445,8 +444,11 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
override suspend fun getPersonalLibrary(): List<LibraryItem>? {
return getMalAnimeListSmart()?.map { it.toLibraryItem() }
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
return SyncAPI.LibraryMetadata(
emptyList(),
getMalAnimeListSmart()?.map { it.toLibraryItem() } ?: emptyList()
)
}
private suspend fun getMalAnimeList(): Array<Data> {

View File

@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
@ -126,7 +127,8 @@ class LibraryFragment : Fragment() {
// If provider
savedSelection.openType == LibraryOpenerType.Provider
&& savedSelection.providerData?.apiName != null -> {
availableProviders.indexOf(savedSelection.providerData.apiName).takeIf { it != -1 }
availableProviders.indexOf(savedSelection.providerData.apiName)
.takeIf { it != -1 }
?.plus(baseOptions.size) ?: -1
}
// Else base option
@ -167,7 +169,7 @@ class LibraryFragment : Fragment() {
viewpager?.setPageTransformer(LibraryScrollTransformer())
viewpager?.adapter =
viewpager.adapter ?: ViewpagerAdapter(emptyList(), { isScrollingDown: Boolean ->
viewpager.adapter ?: ViewpagerAdapter(mutableListOf(), { isScrollingDown: Boolean ->
if (isScrollingDown) {
sort_fab?.shrink()
} else {
@ -177,11 +179,11 @@ class LibraryFragment : Fragment() {
// To prevent future accidents
debugAssert({
searchClickCallback.card !is LibraryItem
searchClickCallback.card !is SyncAPI.LibraryItem
}, {
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
})
val syncId = (searchClickCallback.card as LibraryItem).syncId
val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId
println("SEARCH CLICK $searchClickCallback")
when (searchClickCallback.action) {
@ -200,7 +202,8 @@ class LibraryFragment : Fragment() {
observe(libraryViewModel.pages) { pages ->
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
viewpager.adapter?.notifyItemChanged(viewpager?.currentItem ?: 0)
// Using notifyItemRangeChanged keeps the animations when sorting
viewpager.adapter?.notifyItemRangeChanged(0, viewpager.adapter?.itemCount ?: 0)
TabLayoutMediator(
library_tab_layout,

View File

@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
enum class ListSorting(@StringRes val stringRes: Int) {
@ -20,8 +21,8 @@ enum class ListSorting(@StringRes val stringRes: Int) {
}
class LibraryViewModel : ViewModel() {
private val _pages: MutableLiveData<List<Page>> = MutableLiveData(emptyList())
val pages: LiveData<List<Page>> = _pages
private val _pages: MutableLiveData<List<SyncAPI.Page>> = MutableLiveData(emptyList())
val pages: LiveData<List<SyncAPI.Page>> = _pages
private val _currentApiName: MutableLiveData<String> = MutableLiveData("")
val currentApiName: LiveData<String> = _currentApiName
@ -65,15 +66,22 @@ class LibraryViewModel : ViewModel() {
fun loadPages() {
ioSafe {
currentSyncApi?.let { repo ->
val list = (repo.getPersonalLibrary() as? Resource.Success)?.value
val pages = (list ?: emptyList()).groupBy { it.listName }.map {
Page(
_currentApiName.postValue(repo.name)
val library = (repo.getPersonalLibrary() as? Resource.Success)?.value ?: return@let
val listSubset = library.allLibraryItems.groupBy { it.listName }
val allLists = library.allListNames.associateWith { emptyList<SyncAPI.LibraryItem>() }
val filledLists = allLists + listSubset
val pages = filledLists.map {
SyncAPI.Page(
it.key,
it.value
)
}
println("PAGES $pages")
_pages.postValue(pages)
_currentApiName.postValue(repo.name)
}
}
}

View File

@ -6,16 +6,17 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.utils.AppUtils
import kotlinx.android.synthetic.main.search_result_grid_expanded.view.*
class PageAdapter(
override val items: MutableList<LibraryItem>,
override val items: MutableList<SyncAPI.LibraryItem>,
val clickCallback: (SearchClickCallback) -> Unit
) :
AppUtils.DiffAdapter<LibraryItem>(items) {
AppUtils.DiffAdapter<SyncAPI.LibraryItem>(items) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return LibraryItemViewHolder(
@ -33,7 +34,7 @@ class PageAdapter(
}
inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(item: LibraryItem, position: Int) {
fun bind(item: SyncAPI.LibraryItem, position: Int) {
SearchResultBuilder.bind(
this@PageAdapter.clickCallback,
item,

View File

@ -4,62 +4,16 @@ import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnFlingListener
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import kotlinx.android.synthetic.main.library_viewpager_page.view.*
import me.xdrop.fuzzywuzzy.FuzzySearch
data class Page(
val title: String, var items: List<LibraryItem>
) {
fun sort(method: ListSorting?, query: String? = null) {
items = when (method) {
ListSorting.Query ->
if (query != null) {
items.sortedBy {
-FuzzySearch.partialRatio(
query.lowercase(), it.name.lowercase()
)
}
} else items
ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating ?: 0) }
ListSorting.RatingLow -> items.sortedBy { (it.personalRating ?: 0) }
ListSorting.AlphabeticalA -> items.sortedBy { it.name }
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
else -> items
}
}
}
data class LibraryItem(
override val name: String,
override val url: String,
/** Unique unchanging string used for data storage */
val syncId: String,
val listName: String,
val episodesCompleted: Int?,
val episodesTotal: Int?,
/** Out of 100 */
val personalRating: Int?,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override var posterHeaders: Map<String, String>?,
override var quality: SearchQuality?,
override var id: Int? = null,
) : SearchResponse
class ViewpagerAdapter(
var pages: List<Page>,
var pages: List<SyncAPI.Page>,
val scrollCallback: (isScrollingDown: Boolean) -> Unit,
val clickCallback: (SearchClickCallback) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@ -80,7 +34,7 @@ class ViewpagerAdapter(
inner class PageViewHolder(private val itemViewTest: View) :
RecyclerView.ViewHolder(itemViewTest) {
fun bind(page: Page) {
fun bind(page: SyncAPI.Page) {
if (itemViewTest.page_recyclerview?.adapter == null) {
itemViewTest.page_recyclerview?.adapter = PageAdapter(page.items.toMutableList(), clickCallback)
itemView.page_recyclerview?.spanCount =

View File

@ -277,18 +277,14 @@ object AppUtils {
}
fun updateList(newList: List<T>) {
val time = measureTimeMillis {
val diffResult = DiffUtil.calculateDiff(
GenericDiffCallback(this.items, newList)
)
val diffResult = DiffUtil.calculateDiff(
GenericDiffCallback(this.items, newList)
)
items.clear()
items.addAll(newList)
items.clear()
items.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
println("TIME TAKEn $time")
diffResult.dispatchUpdatesTo(this)
}
inner class GenericDiffCallback(

View File

@ -11,10 +11,8 @@ import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.syncproviders.providers.LocalList
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.LibraryItem
const val VIDEO_POS_DUR = "video_pos_dur"
const val RESULT_WATCH_STATE = "result_watch_state"
@ -54,8 +52,8 @@ object DataStoreHelper {
@JsonProperty("quality") override var quality: SearchQuality? = null,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
) : SearchResponse {
fun toLibraryItem(state: WatchType): LibraryItem {
return LibraryItem(
fun toLibraryItem(state: WatchType): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
name,
url,
url,
@ -86,6 +84,9 @@ object DataStoreHelper {
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
) : SearchResponse
/**
* A datastore wide account for future implementations of a multiple account system
**/
private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
fun getAllWatchStateIds(): List<Int>? {