library work in progress

This commit is contained in:
Blatzar 2022-11-19 20:44:12 +01:00
parent ec9a39814b
commit 67fe6730e0
26 changed files with 692 additions and 90 deletions

View file

@ -177,6 +177,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,

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.ui.library.LibraryItem
interface SyncAPI : OAuth2API { interface SyncAPI : OAuth2API {
val mainUrl: String val mainUrl: String
@ -22,6 +23,8 @@ interface SyncAPI : OAuth2API {
suspend fun search(name: String): List<SyncSearchResult>? suspend fun search(name: String): List<SyncSearchResult>?
suspend fun getPersonalLibrary(): List<LibraryItem>?
fun getIdFromUrl(url : String) : String fun getIdFromUrl(url : String) : String
data class SyncSearchResult( data class SyncSearchResult(

View file

@ -4,6 +4,7 @@ import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.ui.library.LibraryItem
class SyncRepo(private val repo: SyncAPI) { class SyncRepo(private val repo: SyncAPI) {
val idPrefix = repo.idPrefix val idPrefix = repo.idPrefix
@ -16,21 +17,25 @@ class SyncRepo(private val repo: SyncAPI) {
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<List<LibraryItem>> {
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 = repo.getIdFromUrl(url)
} }

View file

@ -13,6 +13,7 @@ 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.ui.library.LibraryItem
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
@ -595,7 +596,26 @@ 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(listName: String?): LibraryItem? {
return LibraryItem(
// English title first
this.media.title.english ?: this.media.title.romaji ?: this.media.synonyms.firstOrNull()
?: "",
this.media.id.toString(),
listName ?: return null,
this.progress,
this.media.episodes,
this.score,
"AniList",
TvType.Anime,
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?,
@ -630,6 +650,10 @@ 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()
}
private suspend fun getFullAnilistList(): FullAnilistList? { private suspend fun getFullAnilistList(): FullAnilistList? {
var userID: Int? = null var userID: Int? = null
/** WARNING ASSUMES ONE USER! **/ /** WARNING ASSUMES ONE USER! **/

View file

@ -7,11 +7,13 @@ 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.ui.library.LibraryItem
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
@ -381,7 +383,22 @@ 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(): LibraryItem {
return LibraryItem(
this.node.title,
this.node.id.toString(),
this.list_status?.status?.lowercase()?.capitalize()?.replace("_", " ") ?: "NONE",
this.list_status?.num_episodes_watched,
this.node.num_episodes,
this.list_status?.score,
"MAL",
TvType.Anime,
this.node.main_picture?.large ?: this.node.main_picture?.medium,
null, null, null
) )
}
}
data class Paging( data class Paging(
@JsonProperty("next") val next: String? @JsonProperty("next") val next: String?
@ -424,6 +441,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
override suspend fun getPersonalLibrary(): List<LibraryItem>? {
return getMalAnimeListSmart()?.map { it.toLibraryItem() }
}
private suspend fun getMalAnimeList(): Array<Data> { private suspend fun getMalAnimeList(): Array<Data> {
checkMalToken() checkMalToken()
var offset = 0 var offset = 0

View file

@ -0,0 +1,106 @@
package com.lagradost.cloudstream3.ui.library
import android.content.Context
import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.activityViewModels
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_library.*
import kotlinx.android.synthetic.main.library_viewpager_page.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class LibraryFragment : Fragment() {
companion object {
fun newInstance() = LibraryFragment()
}
private val libraryViewModel: LibraryViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_library, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(library_root)
val sortView =
menu_toolbar?.menu?.findItem(R.id.sort_button)
sortView?.setOnMenuItemClickListener {
val methods = libraryViewModel.sortingMethods
.map { txt(it.stringRes).asString(context ?: view.context) }
activity?.showBottomDialog(
methods,
libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod),
"Sort by",
false,
{},
{
val method = libraryViewModel.sortingMethods[it]
libraryViewModel.sort(method)
}
)
true
}
val searchView =
menu_toolbar?.menu?.findItem(R.id.search_button)?.actionView as? MenuSearchView
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
libraryViewModel.sort(ListSorting.Query, newText)
return true
}
})
libraryViewModel.loadPages()
viewpager?.setPageTransformer(HomeScrollTransformer())
viewpager?.adapter = viewpager.adapter ?: ViewpagerAdapter(emptyList())
viewpager?.offscreenPageLimit = 10
observe(libraryViewModel.pages) { pages ->
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
viewpager.adapter?.notifyItemChanged(viewpager?.currentItem ?: 0)
TabLayoutMediator(
library_tab_layout,
viewpager,
) { tab, position ->
tab.text = pages.getOrNull(position)?.title
}.attach()
}
}
}
class MenuSearchView(context: Context) : SearchView(context) {
override fun onActionViewCollapsed() {
super.onActionViewCollapsed()
}
}

View file

@ -0,0 +1,74 @@
package com.lagradost.cloudstream3.ui.library
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import me.xdrop.fuzzywuzzy.FuzzySearch
enum class ListSorting(@StringRes val stringRes: Int) {
Query(R.string.none),
RatingHigh(R.string.sort_rating_desc),
RatingLow(R.string.sort_rating_asc),
UpdatedNew(R.string.sort_updated_new),
UpdatedOld(R.string.sort_updated_old),
AlphabeticalA(R.string.sort_alphabetical_a),
AlphabeticalZ(R.string.sort_alphabetical_z),
}
class LibraryViewModel : ViewModel() {
private val _pages: MutableLiveData<List<Page>> = MutableLiveData(emptyList())
val pages: LiveData<List<Page>> = _pages
private val _currentApiName: MutableLiveData<String> = MutableLiveData("")
val currentApiName: LiveData<String> = _currentApiName
private val listApis = SyncApis.filter { it.hasAccount() }
private var currentApi = listApis.firstOrNull()
val sortingMethods = listOf(
ListSorting.RatingHigh,
ListSorting.RatingLow,
// ListSorting.UpdatedNew,
// ListSorting.UpdatedOld,
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
)
var currentSortingMethod: ListSorting = sortingMethods.first()
private set
fun switchList() {
currentApi = listApis[(listApis.indexOf(currentApi) + 1) % listApis.size]
loadPages()
}
fun sort(method: ListSorting, query: String? = null) {
val currentList = pages.value ?: return
currentSortingMethod = method
currentList.forEachIndexed { index, page ->
page.sort(method, query)
}
_pages.postValue(currentList)
}
fun loadPages() {
ioSafe {
currentApi?.let { repo ->
val list = (repo.getPersonalLibrary() as? Resource.Success)?.value
val pages = (list ?: emptyList()).groupBy { it.listName }.map {
Page(
it.key,
it.value
)
}
_pages.postValue(pages)
_currentApiName.postValue(repo.name)
}
}
}
}

View file

@ -0,0 +1,58 @@
package com.lagradost.cloudstream3.ui.library
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.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>,
) :
AppUtils.DiffAdapter<LibraryItem>(items) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return LibraryItemViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.search_result_grid_expanded, parent, false)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is LibraryItemViewHolder -> {
holder.bind(items[position], position)
}
}
}
inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(item: LibraryItem, position: Int) {
SearchResultBuilder.bind(
{ println("CLICKED ${it.action}") },
item,
position,
itemView,
)
// Set watch progress bar
// val showProgress = item.episodesCompleted != null && item.episodesTotal != null
// itemView.watchProgress.isVisible = showProgress
//
// if (showProgress) {
// itemView.watchProgress.max = item.episodesTotal!!
// itemView.watchProgress.progress = item.episodesCompleted!!
// }
itemView.imageText.text = item.name
val showRating = (item.personalRating ?: 0) != 0
itemView.text_rating.isVisible = showRating
if (showRating) {
itemView.text_rating.text = item.personalRating.toString()
}
}
}
}

View file

@ -0,0 +1,85 @@
package com.lagradost.cloudstream3.ui.library
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
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,
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 id: Int?,
override var quality: SearchQuality?,
) : SearchResponse
class ViewpagerAdapter(var pages: List<Page>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PageViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.library_viewpager_page, parent, false)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is PageViewHolder -> {
holder.bind(pages[position])
}
}
}
inner class PageViewHolder(private val itemViewTest: View) :
RecyclerView.ViewHolder(itemViewTest) {
fun bind(page: Page) {
if (itemViewTest.page_recyclerview?.adapter == null) {
itemViewTest.page_recyclerview?.adapter = PageAdapter(page.items.toMutableList())
itemView.page_recyclerview?.spanCount = 4
} else {
(itemViewTest.page_recyclerview?.adapter as? PageAdapter)?.updateList(page.items)
itemViewTest.page_recyclerview?.scrollToPosition(0)
}
}
}
override fun getItemCount(): Int {
return pages.size
}
}

View file

@ -0,0 +1,11 @@
package com.lagradost.cloudstream3.ui.library
import android.view.View
import androidx.viewpager2.widget.ViewPager2
class HomeScrollTransformer : ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) {
}
}

View file

@ -32,6 +32,7 @@ import androidx.core.text.HtmlCompat
import androidx.core.text.toSpanned import androidx.core.text.toSpanned
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
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.PreviewChannelHelper import androidx.tvprovider.media.tv.PreviewChannelHelper
@ -64,6 +65,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) {
@ -263,6 +265,50 @@ 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 time = measureTimeMillis {
val diffResult = DiffUtil.calculateDiff(
GenericDiffCallback(this.items, newList)
)
items.clear()
items.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
println("TIME TAKEn $time")
}
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

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

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/library_root"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<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="wrap_content"
android:layout_alignParentTop="true"
android:background="?attr/primaryGrayBackground"
app:tabIndicatorHeight="30dp"
app:tabIndicatorGravity="center"
app:tabIndicatorColor="@color/textColor"
app:tabIndicator="@drawable/indicator_background"
android:descendantFocusability="blocksDescendants"
android:focusable="false"
app:tabBackground="?attr/primaryGrayBackground"
app:tabGravity="center"
app:tabMode="scrollable"
app:tabTextColor="@color/textColor"
app:tabSelectedTextColor="@color/lightTextColor"
app:tabTextAppearance="@style/TabNoCaps" />
<androidx.viewpager2.widget.ViewPager2 xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_above="@+id/menu_toolbar"
android:layout_below="@+id/library_tab_layout"
android:visibility="visible"
tools:listitem="@layout/library_viewpager_page">
</androidx.viewpager2.widget.ViewPager2>
<!-- <com.google.android.material.appbar.AppBarLayout-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:background="?attr/primaryGrayBackground"-->
<!-- android:backgroundTint="?attr/primaryGrayBackground"-->
<!-- app:elevation="0dp">-->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/menu_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
android:background="?attr/primaryGrayBackground"
android:backgroundTint="?attr/primaryGrayBackground"
app:layout_scrollFlags="scroll|enterAlways"
app:menu="@menu/library_menu">
</com.google.android.material.appbar.MaterialToolbar>
<!-- </com.google.android.material.appbar.AppBarLayout>-->
</RelativeLayout>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
<com.lagradost.cloudstream3.ui.AutofitRecyclerView
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" />
</LinearLayout>

View file

@ -1,90 +1,111 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
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:orientation="vertical"
android:foreground="@drawable/outline_drawable"
android:focusable="true"
android:clickable="true" android:clickable="true"
android:id="@+id/search_result_root"> android:focusable="true"
android:foreground="@drawable/outline_drawable"
android:orientation="vertical">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:layout_margin="2dp" android:id="@+id/background_card"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="2dp"
android:layout_marginBottom="2dp" android:layout_marginBottom="2dp"
android:elevation="10dp" android:elevation="10dp"
app:cardBackgroundColor="?attr/primaryGrayBackground"
app:cardCornerRadius="@dimen/rounded_image_radius" app:cardCornerRadius="@dimen/rounded_image_radius"
android:id="@+id/background_card" app:layout_constraintBottom_toTopOf="@+id/imageText"
app:cardBackgroundColor="?attr/primaryGrayBackground"> app:layout_constraintDimensionRatio="0.6333:1"
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"
tools:src="@drawable/example_poster"
android:duplicateParentState="true"
android:scaleType="centerCrop"
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:duplicateParentState="true"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/search_poster_img_des" /> android:scaleType="centerCrop"
tools:src="@drawable/example_poster" />
<TextView <TextView
tools:text="@string/quality_hd"
android:id="@+id/text_quality" android:id="@+id/text_quality"
android:textColor="@color/textColor"
style="@style/SearchBox" style="@style/SearchBox"
android:background="@drawable/type_bg_color" /> android:background="@drawable/type_bg_color"
android:textColor="@color/textColor"
tools:text="@string/quality_hd" />
<LinearLayout <LinearLayout
android:orientation="vertical"
android:layout_gravity="end"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:layout_gravity="end"
android:orientation="vertical">
<TextView <TextView
android:text="@string/app_dubbed_text"
android:id="@+id/text_is_dub" android:id="@+id/text_is_dub"
android:layout_gravity="end"
style="@style/SearchBox" style="@style/SearchBox"
android:background="@drawable/dub_bg_color" /> android:layout_gravity="end"
android:background="@drawable/dub_bg_color"
android:text="@string/app_dubbed_text" />
<TextView <TextView
android:id="@+id/text_is_sub" android:id="@+id/text_is_sub"
android:text="@string/app_subbed_text"
android:layout_gravity="end"
style="@style/SearchBox" style="@style/SearchBox"
android:background="@drawable/sub_bg_color" /> android:layout_gravity="end"
android:background="@drawable/sub_bg_color"
android:text="@string/app_subbed_text" />
<TextView <TextView
tools:visibility="visible"
android:visibility="gone"
android:textSize="20sp"
android:id="@+id/text_flag" android:id="@+id/text_flag"
tools:text="🇸🇪"
style="@style/SearchBox" style="@style/SearchBox"
android:layout_gravity="end" android:layout_gravity="end"
android:background="@color/transparent" /> android:background="@color/transparent"
android:textSize="20sp"
android:visibility="gone"
tools:text="🇸🇪"
tools:visibility="visible" />
<TextView
android:id="@+id/text_rating"
android:visibility="gone"
tools:visibility="visible"
tools:text="7.7"
style="@style/SearchBox"
android:layout_gravity="end"
android:background="@drawable/rating_bg_color"
android:text="@string/app_subbed_text"
app:drawableStartCompat="@drawable/ic_baseline_star_24" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<TextView <TextView
tools:text="The Perfect Run\nThe Perfect Run" android:id="@+id/imageText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="13sp"
android:gravity="center"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:paddingBottom="5dp" android:layout_weight="0"
android:paddingTop="5dp" android:ellipsize="end"
android:textColor="?attr/textColor" android:gravity="center"
android:id="@+id/imageText"
android:minLines="2"
android:maxLines="2" android:maxLines="2"
android:minLines="2"
android:paddingStart="5dp" android:paddingStart="5dp"
android:paddingTop="5dp"
android:paddingEnd="5dp" android:paddingEnd="5dp"
android:ellipsize="end" /> android:paddingBottom="5dp"
</LinearLayout> android:textColor="?attr/textColor"
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="The Perfect Run\nThe Perfect Run" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,17 +4,21 @@
<item <item
android:id="@+id/navigation_home" android:id="@+id/navigation_home"
android:icon="@drawable/ic_outline_home_24" android:icon="@drawable/ic_outline_home_24"
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_baseline_collections_bookmark_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/ic_outline_settings_24" 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

@ -30,6 +30,7 @@
<color name="subColorBg">#F53B66</color> <color name="subColorBg">#F53B66</color>
<color name="typeColor">#3BF585</color> <color name="typeColor">#3BF585</color>
<color name="typeColorBg">?attr/colorPrimaryDark</color> <color name="typeColorBg">?attr/colorPrimaryDark</color>
<color name="ratingColorBg">#3F51B5</color>
<color name="adultColor">#FF6F63</color> <!-- same as sub color --> <color name="adultColor">#FF6F63</color> <!-- same as sub color -->

View file

@ -253,6 +253,7 @@
<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>
@ -615,4 +616,10 @@
<string name="safe_mode_title">Safe Mode enabled</string> <string name="safe_mode_title">Safe Mode enabled</string>
<string name="safe_mode_description">An unrecoverable crash occurred and we\'ve automatically disabled all extensions, so you can find and remove the extension which is causing trouble.</string> <string name="safe_mode_description">An unrecoverable crash occurred and we\'ve automatically disabled all extensions, so you can find and remove the extension which is causing trouble.</string>
<string name="safe_mode_crash_info">View crash info</string> <string name="safe_mode_crash_info">View crash info</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>
</resources> </resources>

View file

@ -299,6 +299,10 @@
<item name="textAllCaps">false</item> <item name="textAllCaps">false</item>
</style> </style>
<style name="TabNoCaps" parent="TextAppearance.Design.Tab">
<item name="textAllCaps">false</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>