anilist/mal search

This commit is contained in:
LagradOst 2021-11-20 01:41:37 +01:00
parent a8b6fc4e70
commit 28fcf68b14
10 changed files with 396 additions and 45 deletions

View file

@ -3,12 +3,12 @@ package com.lagradost.cloudstream3.syncproviders
import android.content.Context
import com.lagradost.cloudstream3.ShowStatus
interface SyncAPI {
interface SyncAPI : OAuth2API {
data class SyncSearchResult(
val name: String,
val syncApiName: String,
val id: String,
val url: String?,
val url: String,
val posterUrl: String?,
)
@ -64,7 +64,7 @@ interface SyncAPI {
var characters: List<SyncCharacter>? = null,
)
val icon : Int
val icon: Int
val mainUrl: String
fun search(context: Context, name: String): List<SyncSearchResult>?
@ -78,9 +78,9 @@ interface SyncAPI {
4 -> PlanToWatch
5 -> ReWatching
*/
fun score(context: Context, id: String, status : SyncStatus): Boolean
fun score(context: Context, id: String, status: SyncStatus): Boolean
fun getStatus(context: Context, id : String) : SyncStatus?
fun getStatus(context: Context, id: String): SyncStatus?
fun getResult(context: Context, id: String): SyncResult?
}

View file

@ -62,7 +62,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override fun search(context: Context, name: String): List<SyncAPI.SyncSearchResult> {
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
val res = get(
var res = get(
url, headers = mapOf(
"Authorization" to "Bearer " + context.getKey<String>(
accountId,
@ -71,12 +71,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
), cacheTime = 0
).text
return mapper.readValue<MalSearch>(res).data.map {
val node = it.node
SyncAPI.SyncSearchResult(
it.title,
node.title,
this.name,
it.id.toString(),
"$mainUrl/anime/${it.id}/",
it.main_picture?.large ?: it.main_picture?.medium
node.id.toString(),
"$mainUrl/anime/${node.id}/",
node.main_picture?.large ?: node.main_picture?.medium
)
}
}
@ -225,26 +226,26 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String,
@JsonProperty("main_picture") val main_picture: MainPicture?,
@JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles,
@JsonProperty("media_type") val media_type: String,
@JsonProperty("num_episodes") val num_episodes: Int,
@JsonProperty("status") val status: String,
@JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?,
@JsonProperty("media_type") val media_type: String?,
@JsonProperty("num_episodes") val num_episodes: Int?,
@JsonProperty("status") val status: String?,
@JsonProperty("start_date") val start_date: String?,
@JsonProperty("end_date") val end_date: String?,
@JsonProperty("average_episode_duration") val average_episode_duration: Int,
@JsonProperty("synopsis") val synopsis: String,
@JsonProperty("mean") val mean: Double,
@JsonProperty("average_episode_duration") val average_episode_duration: Int?,
@JsonProperty("synopsis") val synopsis: String?,
@JsonProperty("mean") val mean: Double?,
@JsonProperty("genres") val genres: List<Genres>?,
@JsonProperty("rank") val rank: Int,
@JsonProperty("popularity") val popularity: Int,
@JsonProperty("num_list_users") val num_list_users: Int,
@JsonProperty("num_favorites") val num_favorites: Int,
@JsonProperty("num_scoring_users") val num_scoring_users: Int,
@JsonProperty("rank") val rank: Int?,
@JsonProperty("popularity") val popularity: Int?,
@JsonProperty("num_list_users") val num_list_users: Int?,
@JsonProperty("num_favorites") val num_favorites: Int?,
@JsonProperty("num_scoring_users") val num_scoring_users: Int?,
@JsonProperty("start_season") val start_season: StartSeason?,
@JsonProperty("broadcast") val broadcast: Broadcast?,
@JsonProperty("nsfw") val nsfw: String,
@JsonProperty("created_at") val created_at: String,
@JsonProperty("updated_at") val updated_at: String
@JsonProperty("nsfw") val nsfw: String?,
@JsonProperty("created_at") val created_at: String?,
@JsonProperty("updated_at") val updated_at: String?
)
data class ListStatus(
@ -600,14 +601,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
// Used for getDataAboutId()
data class MalAnime(
@JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String,
@JsonProperty("title") val title: String?,
@JsonProperty("num_episodes") val num_episodes: Int,
@JsonProperty("my_list_status") val my_list_status: MalStatus?,
@JsonProperty("main_picture") val main_picture: MalMainPicture?,
)
data class MalSearchNode(
@JsonProperty("node") val node: Node,
)
data class MalSearch(
@JsonProperty("data") val data: List<MalAnime>,
@JsonProperty("data") val data: List<MalSearchNode>,
//paging
)

View file

@ -0,0 +1,178 @@
package com.lagradost.cloudstream3.ui.quicksearch
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.ImageView
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.search.SearchViewModel
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.quick_search.*
import java.util.concurrent.locks.ReentrantLock
class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
companion object {
fun push(activity: Activity?, mainApi: Boolean = true, autoSearch: String? = null) {
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
putBoolean("mainapi", mainApi)
putString("autosearch", autoSearch)
})
}
}
private val searchViewModel: SearchViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
activity?.window?.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
)
return inflater.inflate(R.layout.quick_search, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(quick_search_root)
arguments?.getBoolean("mainapi", true)?.let {
isMainApis = it
}
val listLock = ReentrantLock()
observe(searchViewModel.currentSearch) { list ->
try {
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
listLock.lock()
(quick_search_master_recycler?.adapter as ParentItemAdapter?)?.apply {
items = list.map { ongoing ->
val ongoingList = HomePageList(
ongoing.apiName,
if (ongoing.data is Resource.Success) ongoing.data.value.filterSearchResponse() else ArrayList()
)
ongoingList
}
notifyDataSetChanged()
}
} catch (e: Exception) {
logError(e)
} finally {
listLock.unlock()
}
}
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = ParentItemAdapter(listOf(), { callback ->
when (callback.action) {
SEARCH_ACTION_LOAD -> {
if (isMainApis) {
// this is due to result page only holding 1 thing
activity?.popCurrentPage()
activity?.popCurrentPage()
SearchHelper.handleSearchClickCallback(activity, callback)
} else {
//TODO MAL RESPONSE
}
}
else -> SearchHelper.handleSearchClickCallback(activity, callback)
}
}, { item ->
activity?.loadHomepageList(item)
})
val searchExitIcon = quick_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
val searchMagIcon = quick_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
searchMagIcon.scaleX = 0.65f
searchMagIcon.scaleY = 0.65f
quick_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
context?.let { ctx ->
searchViewModel.searchAndCancel(query = query, context = ctx, isMainApis = isMainApis, ignoreSettings = true)
}
quick_search?.let {
UIHelper.hideKeyboard(it)
}
return true
}
override fun onQueryTextChange(newText: String): Boolean {
//searchViewModel.quickSearch(newText)
return true
}
})
quick_search_loading_bar.alpha = 0f
observe(searchViewModel.searchResponse) {
when (it) {
is Resource.Success -> {
it.value.let { data ->
if (data.isNotEmpty()) {
(cardSpace?.adapter as SearchAdapter?)?.apply {
cardList = data.toList()
notifyDataSetChanged()
}
}
}
searchExitIcon.alpha = 1f
quick_search_loading_bar.alpha = 0f
}
is Resource.Failure -> {
// Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show()
searchExitIcon.alpha = 1f
quick_search_loading_bar.alpha = 0f
}
is Resource.Loading -> {
searchExitIcon.alpha = 0f
quick_search_loading_bar.alpha = 1f
}
}
}
quick_search_master_recycler.adapter = masterAdapter
quick_search_master_recycler.layoutManager = GridLayoutManager(context, 1)
quick_search.setOnQueryTextFocusChangeListener { _, b ->
if (b) {
// https://stackoverflow.com/questions/12022715/unable-to-show-keyboard-automatically-in-the-searchview
UIHelper.showInputMethod(view.findFocus())
}
}
quick_search_back.setOnClickListener {
activity?.popCurrentPage()
}
arguments?.getString("autosearch")?.let {
quick_search.setQuery(it, true)
arguments?.remove("autosearch")
}
}
}

View file

@ -47,6 +47,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownload
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
import com.lagradost.cloudstream3.ui.player.PlayerData
import com.lagradost.cloudstream3.ui.player.PlayerFragment
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
@ -944,6 +945,10 @@ class ResultFragment : Fragment() {
}
}
result_search?.setOnClickListener {
QuickSearchFragment.push(activity,true, d.name)
}
result_share?.setOnClickListener {
val i = Intent(ACTION_SEND)
i.type = "text/plain"

View file

@ -10,7 +10,7 @@ import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.fragment.app.activityViewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -53,15 +53,13 @@ class SearchFragment : Fragment() {
}
}
private lateinit var searchViewModel: SearchViewModel
private val searchViewModel: SearchViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
searchViewModel =
ViewModelProvider(this).get(SearchViewModel::class.java)
activity?.window?.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
)
@ -299,7 +297,10 @@ class SearchFragment : Fragment() {
main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
searchViewModel.searchAndCancel(query)
context?.let { ctx ->
searchViewModel.searchAndCancel(query = query, context = ctx)
}
main_search?.let {
hideKeyboard(it)
}
@ -366,7 +367,7 @@ class SearchFragment : Fragment() {
typesActive = it.getApiTypeSettings()
}
main_search.setOnQueryTextFocusChangeListener { searchView, b ->
main_search.setOnQueryTextFocusChangeListener { _, b ->
if (b) {
// https://stackoverflow.com/questions/12022715/unable-to-show-keyboard-automatically-in-the-searchview
showInputMethod(view.findFocus())

View file

@ -1,13 +1,19 @@
package com.lagradost.cloudstream3.ui.search
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.APIRepository.Companion.providersActive
import kotlinx.coroutines.Dispatchers
@ -28,18 +34,39 @@ class SearchViewModel : ViewModel() {
val currentSearch: LiveData<ArrayList<OnGoingSearch>> get() = _currentSearch
private val repos = apis.map { APIRepository(it) }
private val syncApis = SyncApis
private fun clearSearch() {
_searchResponse.postValue(Resource.Success(ArrayList()))
}
var onGoingSearch: Job? = null
fun searchAndCancel(query: String) {
fun searchAndCancel(query: String, isMainApis : Boolean = true, ignoreSettings : Boolean = false, context: Context) {
onGoingSearch?.cancel()
onGoingSearch = search(query)
onGoingSearch = search(query, isMainApis, ignoreSettings, context)
}
private fun search(query: String) = viewModelScope.launch {
data class SyncSearchResultSearchResponse(
override val name: String,
override val url: String,
override val apiName: String,
override val type: TvType?,
override val posterUrl: String?,
override val id: Int?,
) : SearchResponse
private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse {
return SyncSearchResultSearchResponse(
this.name,
this.url,
this.syncApiName,
null,
this.posterUrl,
null, //this.id.hashCode()
)
}
private fun search(query: String, isMainApis : Boolean = true, ignoreSettings : Boolean = false, context: Context) = viewModelScope.launch {
if (query.length <= 1) {
clearSearch()
return@launch
@ -52,16 +79,25 @@ class SearchViewModel : ViewModel() {
_currentSearch.postValue(ArrayList())
withContext(Dispatchers.IO) { // This interrupts UI otherwise
if (isMainApis) {
repos.filter { a ->
(providersActive.size == 0 || providersActive.contains(a.name))
ignoreSettings || (providersActive.size == 0 || providersActive.contains(a.name))
}.apmap { a -> // Parallel
val search = a.search(query)
currentList.add(OnGoingSearch(a.name,search ))
currentList.add(OnGoingSearch(a.name, search))
_currentSearch.postValue(currentList)
}
} else {
syncApis.apmap { a ->
val search = safeApiCall {
a.search(context, query)?.map { it.toSearchResponse() } ?: throw ErrorLoadingException()
}
_currentSearch.postValue(currentList)
currentList.add(OnGoingSearch(a.name, search))
}
}
}
_currentSearch.postValue(currentList)
val list = ArrayList<SearchResponse>()
val nestedList =

View file

@ -232,7 +232,7 @@
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_descript"
android:nextFocusLeft="@id/result_share"
android:nextFocusRight="@id/result_bookmark_button"
android:nextFocusRight="@id/result_search"
android:id="@+id/result_openinbrower"
android:layout_width="25dp"
@ -247,6 +247,26 @@
android:layout_gravity="center"
android:contentDescription="@string/result_open_in_browser">
</ImageView>
<ImageView
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_descript"
android:nextFocusLeft="@id/result_openinbrower"
android:nextFocusRight="@id/result_bookmark_button"
android:id="@+id/result_search"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_margin="5dp"
android:elevation="10dp"
android:tint="?attr/textColor"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/search_icon"
android:layout_gravity="center"
android:contentDescription="@string/result_open_in_browser">
</ImageView>
</LinearLayout>
</GridLayout>
<TextView

View file

@ -15,7 +15,7 @@
android:layout_margin="10dp"
android:background="@drawable/search_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="45dp"
>
<FrameLayout
android:layout_gravity="center_vertical"

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/quick_search_root"
android:background="?attr/primaryGrayBackground"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical">
<LinearLayout
android:visibility="visible"
android:layout_margin="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<ImageView
android:id="@+id/quick_search_back"
android:layout_gravity="center"
android:foregroundGravity="center"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_arrow_back_24"
app:tint="@android:color/white"
android:layout_width="25dp"
android:layout_height="wrap_content">
<requestFocus/>
</ImageView>
<FrameLayout
android:layout_marginStart="10dp"
android:background="@drawable/search_background"
android:layout_gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="45dp">
<androidx.appcompat.widget.SearchView
android:nextFocusRight="@id/search_filter"
android:nextFocusLeft="@id/search_filter"
android:nextFocusDown="@id/cardSpace"
android:imeOptions="actionSearch"
android:inputType="text"
android:id="@+id/quick_search"
app:queryBackground="@color/transparent"
app:searchIcon="@drawable/search_icon"
android:paddingStart="-10dp"
android:iconifiedByDefault="false"
app:queryHint="@string/search_hint"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
app:iconifiedByDefault="false"
tools:ignore="RtlSymmetry">
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/quick_search_loading_bar"
android:layout_width="20dp" android:layout_height="20dp"
android:layout_marginStart="-35dp"
style="@style/Widget.AppCompat.ProgressBar"
android:foregroundTint="@color/white"
android:progressTint="@color/white"
android:layout_gravity="center">
</androidx.core.widget.ContentLoadingProgressBar>
<!--app:queryHint="@string/search_hint"
android:background="@color/grayBackground" @color/itemBackground
app:searchHintIcon="@drawable/search_white"
-->
</androidx.appcompat.widget.SearchView>
</FrameLayout>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:descendantFocusability="afterDescendants"
android:background="?attr/primaryBlackBackground"
android:id="@+id/quick_search_master_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/homepage_parent"
/>
</LinearLayout>

View file

@ -81,6 +81,25 @@
/>
</action>
<action android:id="@+id/global_to_navigation_quick_search"
app:destination="@id/navigation_quick_search"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim">
<argument
android:name="mainapi"
app:argType="boolean"
android:defaultValue="true"
/>
<argument
android:name="autosearch"
app:argType="string"
android:defaultValue="@null"
/>
</action>
<action android:id="@+id/global_to_navigation_settings"
app:destination="@id/navigation_settings"
app:enterAnim="@anim/enter_anim"
@ -149,6 +168,12 @@
android:name="com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment"
android:label="@string/subtitles_settings"/>
<fragment
android:id="@+id/navigation_quick_search"
android:layout_height="match_parent"
android:name="com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment"
android:label="@string/search"/>
<fragment
android:id="@+id/navigation_download_child"
android:layout_height="match_parent"