diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexAdapter.kt deleted file mode 100644 index 51a72439e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexAdapter.kt +++ /dev/null @@ -1,140 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.index - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.databinding.IndexAdapterBinding - -/** - * Adapter that holds the search cards. - * - * @param controller instance of [IndexController]. - */ -class IndexAdapter(val controller: IndexController) : - RecyclerView.Adapter() { - - val clickListener: ClickListener = controller - - private lateinit var binding: IndexAdapterBinding - - var holder: IndexAdapter.ViewHolder? = null - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IndexAdapter.ViewHolder { - binding = IndexAdapterBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return ViewHolder(binding.root) - } - - override fun onBindViewHolder(holder: IndexAdapter.ViewHolder, position: Int) { - this.holder = holder - holder.bindBrowse(null) - holder.bindLatest(null) - } - - // stores and recycles views as they are scrolled off screen - inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - private val latestAdapter = IndexCardAdapter(controller) - private var latestLastBoundResults: List? = null - - private val browseAdapter = IndexCardAdapter(controller) - private var browseLastBoundResults: List? = null - - init { - binding.browseBarWrapper.setOnClickListener { - clickListener.onBrowseClick() - } - binding.latestBarWrapper.setOnClickListener { - clickListener.onLatestClick() - } - - binding.latestRecycler.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false) - binding.latestRecycler.adapter = latestAdapter - - binding.browseRecycler.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false) - binding.browseRecycler.adapter = browseAdapter - } - - fun bindLatest(latestResults: List?) { - when { - latestResults == null -> { - binding.latestProgress.isVisible = true - showLatestResultsHolder() - } - latestResults.isEmpty() -> { - binding.latestProgress.isVisible = false - showLatestNoResults() - } - else -> { - binding.latestProgress.isVisible = false - showLatestResultsHolder() - } - } - if (latestResults !== latestLastBoundResults) { - latestAdapter.updateDataSet(latestResults) - latestLastBoundResults = latestResults - } - } - - fun bindBrowse(browseResults: List?) { - when { - browseResults == null -> { - binding.browseProgress.isVisible = true - showBrowseResultsHolder() - } - browseResults.isEmpty() -> { - binding.browseProgress.isVisible = false - showBrowseNoResults() - } - else -> { - binding.browseProgress.isVisible = false - showBrowseResultsHolder() - } - } - if (browseResults !== browseLastBoundResults) { - browseAdapter.updateDataSet(browseResults) - browseLastBoundResults = browseResults - } - } - - private fun showLatestResultsHolder() { - binding.latestNoResultsFound.isVisible = false - } - - private fun showLatestNoResults() { - binding.latestNoResultsFound.isVisible = true - } - - private fun showBrowseResultsHolder() { - binding.browseNoResultsFound.isVisible = false - } - - private fun showBrowseNoResults() { - binding.browseNoResultsFound.isVisible = true - } - - fun setLatestImage(manga: Manga) { - latestAdapter.allBoundViewHolders.forEach { - if (it !is IndexCardHolder) return@forEach - if (latestAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach - it.setImage(manga) - } - } - fun setBrowseImage(manga: Manga) { - browseAdapter.allBoundViewHolders.forEach { - if (it !is IndexCardHolder) return@forEach - if (browseAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach - it.setImage(manga) - } - } - } - - interface ClickListener { - fun onBrowseClick(search: String? = null, filters: String? = null) - fun onLatestClick() - } - - override fun getItemCount(): Int = 1 -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt index fe5a1d5c4..876399020 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt @@ -10,12 +10,11 @@ import androidx.appcompat.widget.SearchView import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.databinding.LatestControllerBinding +import eu.kanade.tachiyomi.databinding.IndexControllerBinding import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.FilterList @@ -31,6 +30,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.serialization.json.buildJsonObject +import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.appcompat.QueryTextEvent import reactivecircus.flowbinding.appcompat.queryTextEvents import uy.kohesive.injekt.Injekt @@ -43,10 +43,9 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer * [IndexCardAdapter.OnMangaClickListener] called when manga is clicked in global search */ open class IndexController : - NucleusController, + NucleusController, FabController, - IndexCardAdapter.OnMangaClickListener, - IndexAdapter.ClickListener { + IndexCardAdapter.OnMangaClickListener { constructor(source: CatalogueSource?) : super( bundleOf( @@ -65,13 +64,10 @@ open class IndexController : var source: CatalogueSource? = null - /** - * Adapter containing search results grouped by lang. - */ - protected var adapter: IndexAdapter? = null + private var latestAdapter: IndexCardAdapter? = null + private var browseAdapter: IndexCardAdapter? = null private var actionFab: ExtendedFloatingActionButton? = null - private var actionFabScrollListener: RecyclerView.OnScrollListener? = null /** * Sheet containing filter items. @@ -90,7 +86,7 @@ open class IndexController : * @return inflated view */ override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - binding = LatestControllerBinding.inflate(inflater) + binding = IndexControllerBinding.inflate(inflater) return binding.root } @@ -176,11 +172,39 @@ open class IndexController : // Prepare filter sheet initFilterSheet() - adapter = IndexAdapter(this) + latestAdapter = IndexCardAdapter(this) - // Create recycler and set adapter. - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.adapter = adapter + binding.latestRecycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false) + binding.latestRecycler.adapter = latestAdapter + + browseAdapter = IndexCardAdapter(this) + + binding.browseRecycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false) + binding.browseRecycler.adapter = browseAdapter + + binding.latestBarWrapper.clicks() + .onEach { + onLatestClick() + } + .launchIn(viewScope) + + binding.browseBarWrapper.clicks() + .onEach { + onBrowseClick() + } + .launchIn(viewScope) + + presenter.latestItems + .onEach { + bindLatest(it) + } + .launchIn(viewScope) + + presenter.browseItems + .onEach { + bindBrowse(it) + } + .launchIn(viewScope) presenter.getLatest() } @@ -261,28 +285,91 @@ open class IndexController : override fun cleanupFab(fab: ExtendedFloatingActionButton) { fab.setOnClickListener(null) - actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) } actionFab = null } - fun setLatestManga(results: List?) { - adapter?.holder?.bindLatest(results) + private fun bindLatest(latestResults: List?) { + when { + latestResults == null -> { + binding.latestProgress.isVisible = true + showLatestResultsHolder() + } + latestResults.isEmpty() -> { + binding.latestProgress.isVisible = false + showLatestNoResults() + } + else -> { + binding.latestProgress.isVisible = false + showLatestResultsHolder() + } + } + + latestAdapter?.updateDataSet(latestResults) } - fun setBrowseManga(results: List?) { - adapter?.holder?.bindBrowse(results) + private fun bindBrowse(browseResults: List?) { + when { + browseResults == null -> { + binding.browseProgress.isVisible = true + showBrowseResultsHolder() + } + browseResults.isEmpty() -> { + binding.browseProgress.isVisible = false + showBrowseNoResults() + } + else -> { + binding.browseProgress.isVisible = false + showBrowseResultsHolder() + } + } + + browseAdapter?.updateDataSet(browseResults) + } + + private fun showLatestResultsHolder() { + binding.latestNoResultsFound.isVisible = false + } + + private fun showLatestNoResults() { + binding.latestNoResultsFound.isVisible = true + } + + private fun showBrowseResultsHolder() { + binding.browseNoResultsFound.isVisible = false + } + + private fun showBrowseNoResults() { + binding.browseNoResultsFound.isVisible = true + } + + private fun setLatestImage(manga: Manga) { + val latestAdapter = latestAdapter ?: return + latestAdapter.allBoundViewHolders.forEach { + if (it !is IndexCardHolder) return@forEach + if (latestAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach + it.setImage(manga) + } + } + private fun setBrowseImage(manga: Manga) { + val browseAdapter = browseAdapter ?: return + browseAdapter.allBoundViewHolders.forEach { + if (it !is IndexCardHolder) return@forEach + if (browseAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach + it.setImage(manga) + } } override fun onDestroyView(view: View) { - adapter = null + latestAdapter = null + browseAdapter = null super.onDestroyView(view) } - override fun onBrowseClick(search: String?, filters: String?) { + private fun onBrowseClick(search: String? = null, filters: String? = null) { router.replaceTopController(BrowseSourceController(presenter.source, search, filterList = filters).withFadeTransaction()) } - override fun onLatestClick() { + private fun onLatestClick() { router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction()) } @@ -292,8 +379,8 @@ open class IndexController : * @param manga the initialized manga. */ fun onMangaInitialized(manga: Manga, isLatest: Boolean) { - if (isLatest) adapter?.holder?.setLatestImage(manga) - else adapter?.holder?.setBrowseImage(manga) + if (isLatest) setLatestImage(manga) + else setBrowseImage(manga) } companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexPresenter.kt index 019f96a60..a030630ce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexPresenter.kt @@ -15,10 +15,10 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Companion.toItems import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.runAsObservable -import eu.kanade.tachiyomi.util.lang.withUIContext import exh.savedsearches.EXHSavedSearch import exh.savedsearches.JsonSavedSearch import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json @@ -78,6 +78,10 @@ open class IndexPresenter( */ private var fetchImageSubscription: Subscription? = null + val latestItems = MutableStateFlow?>(null) + + val browseItems = MutableStateFlow?>(null) + override fun onDestroy() { fetchSourcesSubscription?.unsubscribe() fetchImageSubscription?.unsubscribe() @@ -98,54 +102,37 @@ open class IndexPresenter( initializeFetchImageSubscription() presenterScope.launch(Dispatchers.IO) { - withUIContext { - Observable.just(null).subscribeLatestCache({ view, results -> - view.setLatestManga(results) - }) - } - if (source.supportsLatest) { - val results = try { + if (latestItems.value != null) return@launch + val results = if (source.supportsLatest) { + try { source.fetchLatestUpdates(1) .awaitSingle() .mangas - .take(10) .map { networkToLocalManga(it, source.id) } } catch (e: Exception) { emptyList() } - fetchImage(results, true) + } else emptyList() - withUIContext { - Observable.just(results.map { IndexCardItem(it) }).subscribeLatestCache({ view, results -> - view.setLatestManga(results) - }) - } - } + fetchImage(results, true) + + latestItems.value = results.map { IndexCardItem(it) } } presenterScope.launch(Dispatchers.IO) { - withUIContext { - Observable.just(null).subscribeLatestCache({ view, results -> - view.setBrowseManga(results) - }) - } - + if (browseItems.value != null) return@launch val results = try { source.fetchPopularManga(1) .awaitSingle() .mangas - .take(10) .map { networkToLocalManga(it, source.id) } } catch (e: Exception) { emptyList() } + fetchImage(results, false) - withUIContext { - Observable.just(results.map { IndexCardItem(it) }).subscribeLatestCache({ view, results -> - view.setBrowseManga(results) - }) - } + browseItems.value = results.map { IndexCardItem(it) } } } @@ -221,10 +208,10 @@ open class IndexPresenter( fun loadSearches(): List { val loaded = preferences.savedSearches().get() - return loaded.map { + return loaded.mapNotNull { try { val id = it.substringBefore(':').toLong() - if (id != source.id) return@map null + if (id != source.id) return@mapNotNull null val content = Json.decodeFromString(it.substringAfter(':')) val originalFilters = source.getFilterList() filterSerializer.deserialize(originalFilters, content.filters) @@ -239,6 +226,6 @@ open class IndexPresenter( t.printStackTrace() null } - }.filterNotNull() + } } } diff --git a/app/src/main/res/layout/index_adapter.xml b/app/src/main/res/layout/index_controller.xml similarity index 100% rename from app/src/main/res/layout/index_adapter.xml rename to app/src/main/res/layout/index_controller.xml