From 28fcf68b1423507436560d3ccae95a201695faa3 Mon Sep 17 00:00:00 2001 From: LagradOst Date: Sat, 20 Nov 2021 01:41:37 +0100 Subject: [PATCH] anilist/mal search --- .../cloudstream3/syncproviders/SyncAPI.kt | 10 +- .../syncproviders/providers/MALApi.kt | 49 ++--- .../ui/quicksearch/QuickSearchFragment.kt | 178 ++++++++++++++++++ .../cloudstream3/ui/result/ResultFragment.kt | 5 + .../cloudstream3/ui/search/SearchFragment.kt | 13 +- .../cloudstream3/ui/search/SearchViewModel.kt | 56 +++++- app/src/main/res/layout/fragment_result.xml | 22 ++- app/src/main/res/layout/fragment_search.xml | 2 +- app/src/main/res/layout/quick_search.xml | 81 ++++++++ .../main/res/navigation/mobile_navigation.xml | 25 +++ 10 files changed, 396 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt create mode 100644 app/src/main/res/layout/quick_search.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index 6023e3e3..bacd982e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -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? = null, ) - val icon : Int + val icon: Int val mainUrl: String fun search(context: Context, name: String): List? @@ -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? } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index ff7ced20..d2cd009b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -62,7 +62,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override fun search(context: Context, name: String): List { 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( accountId, @@ -71,12 +71,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ), cacheTime = 0 ).text return mapper.readValue(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?, - @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, + @JsonProperty("data") val data: List, //paging ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt new file mode 100644 index 00000000..0a47b3de --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -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 = 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(androidx.appcompat.R.id.search_close_btn) + val searchMagIcon = quick_search.findViewById(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") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index caa9009b..46edf989 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -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" diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index caa9d78f..799d6603 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -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()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index a9aa5185..889b3a92 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -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> 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,17 +79,26 @@ class SearchViewModel : ViewModel() { _currentSearch.postValue(ArrayList()) withContext(Dispatchers.IO) { // This interrupts UI otherwise - repos.filter { a -> - (providersActive.size == 0 || providersActive.contains(a.name)) - }.apmap { a -> // Parallel - val search = a.search(query) - currentList.add(OnGoingSearch(a.name,search )) - _currentSearch.postValue(currentList) + if (isMainApis) { + repos.filter { a -> + ignoreSettings || (providersActive.size == 0 || providersActive.contains(a.name)) + }.apmap { a -> // Parallel + val search = a.search(query) + 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() + } + + currentList.add(OnGoingSearch(a.name, search)) + } } } _currentSearch.postValue(currentList) - val list = ArrayList() val nestedList = currentList.map { it.data }.filterIsInstance>>().map { it.value } diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 0c70464c..2751a0de 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -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"> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index f3dd961c..38f196ce 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -81,6 +81,25 @@ /> + + + + + + +