Convert Source Feed to Jetpack Compose
This commit is contained in:
parent
493a1ab4a6
commit
a760198981
@ -0,0 +1,219 @@
|
|||||||
|
package eu.kanade.presentation.browse
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
|
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||||
|
import eu.kanade.presentation.util.bottomNavPaddingValues
|
||||||
|
import eu.kanade.presentation.util.plus
|
||||||
|
import eu.kanade.presentation.util.topPaddingValues
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedPresenter
|
||||||
|
import exh.savedsearches.models.FeedSavedSearch
|
||||||
|
import exh.savedsearches.models.SavedSearch
|
||||||
|
|
||||||
|
sealed class SourceFeedUI {
|
||||||
|
abstract val id: Long
|
||||||
|
|
||||||
|
abstract val title: String
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get
|
||||||
|
|
||||||
|
abstract val results: List<Manga>?
|
||||||
|
|
||||||
|
abstract fun withResults(results: List<Manga>?): SourceFeedUI
|
||||||
|
|
||||||
|
data class Latest(override val results: List<Manga>?) : SourceFeedUI() {
|
||||||
|
override val id: Long = -1
|
||||||
|
override val title: String
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = stringResource(R.string.latest)
|
||||||
|
|
||||||
|
override fun withResults(results: List<Manga>?): SourceFeedUI {
|
||||||
|
return copy(results = results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class Browse(override val results: List<Manga>?) : SourceFeedUI() {
|
||||||
|
override val id: Long = -2
|
||||||
|
override val title: String
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = stringResource(R.string.browse)
|
||||||
|
|
||||||
|
override fun withResults(results: List<Manga>?): SourceFeedUI {
|
||||||
|
return copy(results = results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class SourceSavedSearch(
|
||||||
|
val feed: FeedSavedSearch,
|
||||||
|
val savedSearch: SavedSearch,
|
||||||
|
override val results: List<Manga>?,
|
||||||
|
) : SourceFeedUI() {
|
||||||
|
override val id: Long
|
||||||
|
get() = feed.id
|
||||||
|
|
||||||
|
override val title: String
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = savedSearch.name
|
||||||
|
|
||||||
|
override fun withResults(results: List<Manga>?): SourceFeedUI {
|
||||||
|
return copy(results = results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceFeedScreen(
|
||||||
|
nestedScrollInterop: NestedScrollConnection,
|
||||||
|
presenter: SourceFeedPresenter,
|
||||||
|
onClickBrowse: () -> Unit,
|
||||||
|
onClickLatest: () -> Unit,
|
||||||
|
onClickSavedSearch: (SavedSearch) -> Unit,
|
||||||
|
onClickDelete: (FeedSavedSearch) -> Unit,
|
||||||
|
onClickManga: (Manga) -> Unit,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
presenter.isLoading -> LoadingScreen()
|
||||||
|
else -> {
|
||||||
|
SourceFeedList(
|
||||||
|
nestedScrollConnection = nestedScrollInterop,
|
||||||
|
state = presenter,
|
||||||
|
onClickBrowse = onClickBrowse,
|
||||||
|
onClickLatest = onClickLatest,
|
||||||
|
onClickSavedSearch = onClickSavedSearch,
|
||||||
|
onClickDelete = onClickDelete,
|
||||||
|
onClickManga = onClickManga,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceFeedList(
|
||||||
|
nestedScrollConnection: NestedScrollConnection,
|
||||||
|
state: SourceFeedState,
|
||||||
|
onClickBrowse: () -> Unit,
|
||||||
|
onClickLatest: () -> Unit,
|
||||||
|
onClickSavedSearch: (SavedSearch) -> Unit,
|
||||||
|
onClickDelete: (FeedSavedSearch) -> Unit,
|
||||||
|
onClickManga: (Manga) -> Unit,
|
||||||
|
) {
|
||||||
|
ScrollbarLazyColumn(
|
||||||
|
modifier = Modifier.nestedScroll(nestedScrollConnection),
|
||||||
|
contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
state.items.orEmpty(),
|
||||||
|
key = { it.id },
|
||||||
|
) { item ->
|
||||||
|
SourceFeedItem(
|
||||||
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
item = item,
|
||||||
|
onClickTitle = when (item) {
|
||||||
|
is SourceFeedUI.Browse -> onClickBrowse
|
||||||
|
is SourceFeedUI.Latest -> onClickLatest
|
||||||
|
is SourceFeedUI.SourceSavedSearch -> {
|
||||||
|
{ onClickSavedSearch(item.savedSearch) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickDelete = onClickDelete,
|
||||||
|
onClickManga = onClickManga,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceFeedItem(
|
||||||
|
modifier: Modifier,
|
||||||
|
item: SourceFeedUI,
|
||||||
|
onClickTitle: () -> Unit,
|
||||||
|
onClickDelete: (FeedSavedSearch) -> Unit,
|
||||||
|
onClickManga: (Manga) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier then Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.let {
|
||||||
|
if (item is SourceFeedUI.SourceSavedSearch) {
|
||||||
|
it.combinedClickable(
|
||||||
|
onLongClick = {
|
||||||
|
onClickDelete(item.feed)
|
||||||
|
},
|
||||||
|
onClick = onClickTitle,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
it.clickable(onClick = onClickTitle)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(start = 16.dp)) {
|
||||||
|
Text(
|
||||||
|
text = item.title,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_arrow_forward_24dp),
|
||||||
|
contentDescription = stringResource(R.string.label_more),
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val results = item.results
|
||||||
|
when {
|
||||||
|
results == null -> {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
results.isEmpty() -> {
|
||||||
|
Text(stringResource(R.string.no_results_found), modifier = Modifier.padding(bottom = 16.dp))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
LazyRow(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||||
|
) {
|
||||||
|
items(results) {
|
||||||
|
FeedCardItem(
|
||||||
|
manga = it,
|
||||||
|
onClickManga = onClickManga,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package eu.kanade.presentation.browse
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
interface SourceFeedState {
|
||||||
|
val isLoading: Boolean
|
||||||
|
val items: List<SourceFeedUI>?
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SourceFeedState(): SourceFeedState {
|
||||||
|
return SourceFeedStateImpl()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SourceFeedStateImpl : SourceFeedState {
|
||||||
|
override var isLoading: Boolean by mutableStateOf(true)
|
||||||
|
override var items: List<SourceFeedUI>? by mutableStateOf(null)
|
||||||
|
}
|
@ -1,85 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.feed
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.util.SparseArray
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import exh.savedsearches.models.FeedSavedSearch
|
|
||||||
import exh.savedsearches.models.SavedSearch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter that holds the search cards.
|
|
||||||
*
|
|
||||||
* @param controller instance of [SourceFeedController].
|
|
||||||
*/
|
|
||||||
class SourceFeedAdapter(val controller: SourceFeedController) :
|
|
||||||
FlexibleAdapter<SourceFeedItem>(null, controller, true) {
|
|
||||||
|
|
||||||
val feedClickListener: OnFeedClickListener = controller
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bundle where the view state of the holders is saved.
|
|
||||||
*/
|
|
||||||
private var bundle = Bundle()
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any?>) {
|
|
||||||
super.onBindViewHolder(holder, position, payloads)
|
|
||||||
restoreHolderState(holder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
|
||||||
super.onViewRecycled(holder)
|
|
||||||
saveHolderState(holder, bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
val holdersBundle = Bundle()
|
|
||||||
allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) }
|
|
||||||
outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
|
||||||
bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the view state of the given holder.
|
|
||||||
*
|
|
||||||
* @param holder The holder to save.
|
|
||||||
* @param outState The bundle where the state is saved.
|
|
||||||
*/
|
|
||||||
private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) {
|
|
||||||
val key = "holder_${holder.bindingAdapterPosition}"
|
|
||||||
val holderState = SparseArray<Parcelable>()
|
|
||||||
holder.itemView.saveHierarchyState(holderState)
|
|
||||||
outState.putSparseParcelableArray(key, holderState)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the view state of the given holder.
|
|
||||||
*
|
|
||||||
* @param holder The holder to restore.
|
|
||||||
*/
|
|
||||||
private fun restoreHolderState(holder: RecyclerView.ViewHolder) {
|
|
||||||
val key = "holder_${holder.bindingAdapterPosition}"
|
|
||||||
val holderState = bundle.getSparseParcelableArray<Parcelable>(key)
|
|
||||||
if (holderState != null) {
|
|
||||||
holder.itemView.restoreHierarchyState(holderState)
|
|
||||||
bundle.remove(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnFeedClickListener {
|
|
||||||
fun onLatestClick()
|
|
||||||
fun onBrowseClick()
|
|
||||||
fun onSavedSearchClick(savedSearch: SavedSearch)
|
|
||||||
fun onRemoveClick(feedSavedSearch: FeedSavedSearch)
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val HOLDER_BUNDLE_KEY = "holder_bundle"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.feed
|
|
||||||
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter that holds the manga items from search results.
|
|
||||||
*
|
|
||||||
* @param controller instance of [SourceFeedController].
|
|
||||||
*/
|
|
||||||
class SourceFeedCardAdapter(controller: SourceFeedController) :
|
|
||||||
FlexibleAdapter<SourceFeedCardItem>(null, controller, true) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen for browse item clicks.
|
|
||||||
*/
|
|
||||||
val mangaClickListener: OnMangaClickListener = controller
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener which should be called when user clicks browse.
|
|
||||||
* Note: Should only be handled by [SourceFeedController]
|
|
||||||
*/
|
|
||||||
interface OnMangaClickListener {
|
|
||||||
fun onMangaClick(manga: Manga)
|
|
||||||
fun onMangaLongClick(manga: Manga)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.feed
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import coil.dispose
|
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding
|
|
||||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
|
||||||
|
|
||||||
class SourceFeedCardHolder(view: View, adapter: SourceFeedCardAdapter) :
|
|
||||||
FlexibleViewHolder(view, adapter) {
|
|
||||||
|
|
||||||
private val binding = GlobalSearchControllerCardItemBinding.bind(view)
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Call onMangaClickListener when item is pressed.
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
val item = adapter.getItem(bindingAdapterPosition)
|
|
||||||
if (item != null) {
|
|
||||||
adapter.mangaClickListener.onMangaClick(item.manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
itemView.setOnLongClickListener {
|
|
||||||
val item = adapter.getItem(bindingAdapterPosition)
|
|
||||||
if (item != null) {
|
|
||||||
adapter.mangaClickListener.onMangaLongClick(item.manga)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(manga: Manga) {
|
|
||||||
binding.card.clipToOutline = true
|
|
||||||
|
|
||||||
// Set manga title
|
|
||||||
binding.title.text = manga.title
|
|
||||||
|
|
||||||
// Set alpha of thumbnail.
|
|
||||||
binding.cover.alpha = if (manga.favorite) 0.3f else 1.0f
|
|
||||||
|
|
||||||
// For rounded corners
|
|
||||||
binding.badges.clipToOutline = true
|
|
||||||
|
|
||||||
// Set favorite badge
|
|
||||||
binding.favoriteText.isVisible = manga.favorite
|
|
||||||
|
|
||||||
setImage(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setImage(manga: Manga) {
|
|
||||||
binding.cover.dispose()
|
|
||||||
binding.cover.loadAutoPause(manga) {
|
|
||||||
setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.feed
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
|
|
||||||
class SourceFeedCardItem(val manga: Manga) : AbstractFlexibleItem<SourceFeedCardHolder>() {
|
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
|
||||||
return R.layout.global_search_controller_card_item
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceFeedCardHolder {
|
|
||||||
return SourceFeedCardHolder(view, adapter as SourceFeedCardAdapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bindViewHolder(
|
|
||||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
|
||||||
holder: SourceFeedCardHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: List<Any?>?,
|
|
||||||
) {
|
|
||||||
holder.bind(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other is SourceFeedCardItem) {
|
|
||||||
return manga.id == other.manga.id
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return manga.id?.toInt() ?: 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +1,23 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.feed
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.presentation.browse.SourceFeedScreen
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.SearchableComposeController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
|
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
|
||||||
@ -42,10 +41,8 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
|||||||
* [SourceFeedCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
* [SourceFeedCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||||
*/
|
*/
|
||||||
open class SourceFeedController :
|
open class SourceFeedController :
|
||||||
SearchableNucleusController<GlobalSearchControllerBinding, SourceFeedPresenter>,
|
SearchableComposeController<SourceFeedPresenter>,
|
||||||
FabController,
|
FabController {
|
||||||
SourceFeedCardAdapter.OnMangaClickListener,
|
|
||||||
SourceFeedAdapter.OnFeedClickListener {
|
|
||||||
|
|
||||||
constructor(source: CatalogueSource?) : super(
|
constructor(source: CatalogueSource?) : super(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
@ -62,11 +59,6 @@ open class SourceFeedController :
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
constructor(bundle: Bundle) : this(bundle.getLong(SOURCE_EXTRA))
|
constructor(bundle: Bundle) : this(bundle.getLong(SOURCE_EXTRA))
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter containing search results grouped by lang.
|
|
||||||
*/
|
|
||||||
protected var adapter: SourceFeedAdapter? = null
|
|
||||||
|
|
||||||
var source: CatalogueSource? = null
|
var source: CatalogueSource? = null
|
||||||
|
|
||||||
private var actionFab: ExtendedFloatingActionButton? = null
|
private var actionFab: ExtendedFloatingActionButton? = null
|
||||||
@ -90,27 +82,7 @@ open class SourceFeedController :
|
|||||||
* @return instance of [SourceFeedPresenter]
|
* @return instance of [SourceFeedPresenter]
|
||||||
*/
|
*/
|
||||||
override fun createPresenter(): SourceFeedPresenter {
|
override fun createPresenter(): SourceFeedPresenter {
|
||||||
return SourceFeedPresenter(source!!)
|
return SourceFeedPresenter(source = source!!)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when manga in global search is clicked, opens manga.
|
|
||||||
*
|
|
||||||
* @param manga clicked item containing manga information.
|
|
||||||
*/
|
|
||||||
override fun onMangaClick(manga: Manga) {
|
|
||||||
// Open MangaController.
|
|
||||||
router.pushController(MangaController(manga.id!!, true).withFadeTransaction())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when manga in global search is long clicked.
|
|
||||||
*
|
|
||||||
* @param manga clicked item containing manga information.
|
|
||||||
*/
|
|
||||||
override fun onMangaLongClick(manga: Manga) {
|
|
||||||
// Delegate to single click by default.
|
|
||||||
onMangaClick(manga)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,8 +105,6 @@ open class SourceFeedController :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createBinding(inflater: LayoutInflater): GlobalSearchControllerBinding = GlobalSearchControllerBinding.inflate(inflater)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the view is created
|
* Called when the view is created
|
||||||
*
|
*
|
||||||
@ -145,33 +115,6 @@ open class SourceFeedController :
|
|||||||
|
|
||||||
// Prepare filter sheet
|
// Prepare filter sheet
|
||||||
initFilterSheet()
|
initFilterSheet()
|
||||||
|
|
||||||
binding.recycler.applyInsetter {
|
|
||||||
type(navigationBars = true) {
|
|
||||||
padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter = SourceFeedAdapter(this)
|
|
||||||
|
|
||||||
// Create recycler and set adapter.
|
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
|
||||||
binding.recycler.adapter = adapter
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
|
||||||
adapter = null
|
|
||||||
super.onDestroyView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveViewState(view: View, outState: Bundle) {
|
|
||||||
super.onSaveViewState(view, outState)
|
|
||||||
adapter?.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
|
|
||||||
super.onRestoreViewState(view, savedViewState)
|
|
||||||
adapter?.onRestoreInstanceState(savedViewState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val filterSerializer = FilterSerializer()
|
private val filterSerializer = FilterSerializer()
|
||||||
@ -284,66 +227,46 @@ open class SourceFeedController :
|
|||||||
actionFab = null
|
actionFab = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Composable
|
||||||
* Returns the view holder for the given manga.
|
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
|
||||||
*
|
SourceFeedScreen(
|
||||||
* @param source used to find holder containing source
|
nestedScrollInterop = nestedScrollInterop,
|
||||||
* @return the holder of the manga or null if it's not bound.
|
presenter = presenter,
|
||||||
*/
|
onClickBrowse = ::onBrowseClick,
|
||||||
private fun getHolder(sourceFeed: SourceFeed): SourceFeedHolder? {
|
onClickLatest = ::onLatestClick,
|
||||||
val adapter = adapter ?: return null
|
onClickSavedSearch = ::onSavedSearchClick,
|
||||||
|
onClickDelete = ::onRemoveClick,
|
||||||
adapter.allBoundViewHolders.forEach { holder ->
|
onClickManga = ::onMangaClick,
|
||||||
val item = adapter.getItem(holder.bindingAdapterPosition)
|
)
|
||||||
if (item != null && sourceFeed == item.sourceFeed) {
|
|
||||||
return holder as SourceFeedHolder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add search result to adapter.
|
* Called when manga in global search is clicked, opens manga.
|
||||||
*
|
*
|
||||||
* @param feedManga the source items containing the latest manga.
|
* @param manga clicked item containing manga information.
|
||||||
*/
|
*/
|
||||||
fun setItems(feedManga: List<SourceFeedItem>) {
|
private fun onMangaClick(manga: Manga) {
|
||||||
adapter?.updateDataSet(feedManga)
|
// Open MangaController.
|
||||||
|
router.pushController(MangaController(manga.id, true).withFadeTransaction())
|
||||||
if (feedManga.isEmpty()) {
|
|
||||||
binding.emptyView.show(R.string.feed_tab_empty)
|
|
||||||
} else {
|
|
||||||
binding.emptyView.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when a manga is initialized.
|
|
||||||
*
|
|
||||||
* @param manga the initialized manga.
|
|
||||||
*/
|
|
||||||
fun onMangaInitialized(sourceFeed: SourceFeed, manga: Manga) {
|
|
||||||
getHolder(sourceFeed)?.setImage(manga)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBrowseClick(search: String? = null, savedSearch: Long? = null, filters: String? = null) {
|
fun onBrowseClick(search: String? = null, savedSearch: Long? = null, filters: String? = null) {
|
||||||
router.replaceTopController(BrowseSourceController(presenter.source, search, savedSearch = savedSearch, filterList = filters).withFadeTransaction())
|
router.replaceTopController(BrowseSourceController(presenter.source, search, savedSearch = savedSearch, filterList = filters).withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLatestClick() {
|
private fun onLatestClick() {
|
||||||
router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction())
|
router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBrowseClick() {
|
private fun onBrowseClick() {
|
||||||
router.replaceTopController(BrowseSourceController(presenter.source).withFadeTransaction())
|
router.replaceTopController(BrowseSourceController(presenter.source).withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSavedSearchClick(savedSearch: SavedSearch) {
|
private fun onSavedSearchClick(savedSearch: SavedSearch) {
|
||||||
router.replaceTopController(BrowseSourceController(presenter.source, savedSearch = savedSearch.id).withFadeTransaction())
|
router.replaceTopController(BrowseSourceController(presenter.source, savedSearch = savedSearch.id).withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRemoveClick(feedSavedSearch: FeedSavedSearch) {
|
private fun onRemoveClick(feedSavedSearch: FeedSavedSearch) {
|
||||||
MaterialAlertDialogBuilder(activity!!)
|
MaterialAlertDialogBuilder(activity!!)
|
||||||
.setTitle(R.string.feed)
|
.setTitle(R.string.feed)
|
||||||
.setMessage(R.string.feed_delete)
|
.setMessage(R.string.feed_delete)
|
||||||
|
@ -1,125 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.feed
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardBinding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holder that binds the [SourceFeedItem] containing catalogue cards.
|
|
||||||
*
|
|
||||||
* @param view view of [SourceFeedItem]
|
|
||||||
* @param adapter instance of [SourceFeedAdapter]
|
|
||||||
*/
|
|
||||||
class SourceFeedHolder(view: View, val adapter: SourceFeedAdapter) :
|
|
||||||
FlexibleViewHolder(view, adapter) {
|
|
||||||
|
|
||||||
private val binding = GlobalSearchControllerCardBinding.bind(view)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter containing manga from search results.
|
|
||||||
*/
|
|
||||||
private val mangaAdapter = SourceFeedCardAdapter(adapter.controller)
|
|
||||||
|
|
||||||
private var lastBoundResults: List<SourceFeedCardItem>? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Set layout horizontal.
|
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
|
||||||
binding.recycler.adapter = mangaAdapter
|
|
||||||
|
|
||||||
binding.titleWrapper.setOnClickListener {
|
|
||||||
adapter.getItem(bindingAdapterPosition)?.let {
|
|
||||||
when (it.sourceFeed) {
|
|
||||||
SourceFeed.Browse -> adapter.feedClickListener.onBrowseClick()
|
|
||||||
SourceFeed.Latest -> adapter.feedClickListener.onLatestClick()
|
|
||||||
is SourceFeed.SourceSavedSearch -> adapter.feedClickListener.onSavedSearchClick(it.sourceFeed.savedSearch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.titleWrapper.setOnLongClickListener {
|
|
||||||
adapter.getItem(bindingAdapterPosition)?.let {
|
|
||||||
when (it.sourceFeed) {
|
|
||||||
SourceFeed.Browse -> adapter.feedClickListener.onBrowseClick()
|
|
||||||
SourceFeed.Latest -> adapter.feedClickListener.onLatestClick()
|
|
||||||
is SourceFeed.SourceSavedSearch -> adapter.feedClickListener.onRemoveClick(it.sourceFeed.feed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the loading of source search result.
|
|
||||||
*
|
|
||||||
* @param item item of card.
|
|
||||||
*/
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
fun bind(item: SourceFeedItem) {
|
|
||||||
val results = item.results
|
|
||||||
|
|
||||||
when (item.sourceFeed) {
|
|
||||||
SourceFeed.Browse -> binding.title.setText(R.string.browse)
|
|
||||||
SourceFeed.Latest -> binding.title.setText(R.string.latest)
|
|
||||||
is SourceFeed.SourceSavedSearch -> binding.title.text = item.sourceFeed.savedSearch.name
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
|
||||||
results == null -> {
|
|
||||||
binding.progress.isVisible = true
|
|
||||||
showResultsHolder()
|
|
||||||
}
|
|
||||||
results.isEmpty() -> {
|
|
||||||
binding.progress.isVisible = false
|
|
||||||
showNoResults()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
binding.progress.isVisible = false
|
|
||||||
showResultsHolder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (results !== lastBoundResults) {
|
|
||||||
mangaAdapter.updateDataSet(results)
|
|
||||||
lastBoundResults = results
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when a manga is initialized.
|
|
||||||
*
|
|
||||||
* @param manga the initialized manga.
|
|
||||||
*/
|
|
||||||
fun setImage(manga: Manga) {
|
|
||||||
getHolder(manga)?.setImage(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the view holder for the given manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga to find.
|
|
||||||
* @return the holder of the manga or null if it's not bound.
|
|
||||||
*/
|
|
||||||
private fun getHolder(manga: Manga): SourceFeedCardHolder? {
|
|
||||||
mangaAdapter.allBoundViewHolders.forEach { holder ->
|
|
||||||
val item = mangaAdapter.getItem(holder.bindingAdapterPosition)
|
|
||||||
if (item != null && item.manga.id!! == manga.id!!) {
|
|
||||||
return holder as SourceFeedCardHolder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showResultsHolder() {
|
|
||||||
binding.noResultsFound.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNoResults() {
|
|
||||||
binding.noResultsFound.isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.feed
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item that contains search result information.
|
|
||||||
*
|
|
||||||
* @param feed the source for the search results.
|
|
||||||
* @param results the search results.
|
|
||||||
* @param highlighted whether this search item should be highlighted/marked in the catalogue search view.
|
|
||||||
*/
|
|
||||||
class SourceFeedItem(
|
|
||||||
val sourceFeed: SourceFeed,
|
|
||||||
val results: List<SourceFeedCardItem>?,
|
|
||||||
val highlighted: Boolean = false,
|
|
||||||
) : AbstractFlexibleItem<SourceFeedHolder>() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set view.
|
|
||||||
*
|
|
||||||
* @return id of view
|
|
||||||
*/
|
|
||||||
override fun getLayoutRes(): Int {
|
|
||||||
return R.layout.global_search_controller_card
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create view holder (see [SourceFeedAdapter].
|
|
||||||
*
|
|
||||||
* @return holder of view.
|
|
||||||
*/
|
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceFeedHolder {
|
|
||||||
return SourceFeedHolder(view, adapter as SourceFeedAdapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind item to view.
|
|
||||||
*/
|
|
||||||
override fun bindViewHolder(
|
|
||||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
|
||||||
holder: SourceFeedHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: List<Any?>?,
|
|
||||||
) {
|
|
||||||
holder.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to check if two items are equal.
|
|
||||||
*
|
|
||||||
* @return items are equal?
|
|
||||||
*/
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other is SourceFeedItem) {
|
|
||||||
return sourceFeed == other.sourceFeed
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return hash code of item.
|
|
||||||
*
|
|
||||||
* @return hashcode
|
|
||||||
*/
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return sourceFeed.hashCode()
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,6 +14,9 @@ import eu.kanade.domain.source.interactor.GetFeedSavedSearchBySourceId
|
|||||||
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceId
|
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceId
|
||||||
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceIdFeed
|
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceIdFeed
|
||||||
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
|
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
|
||||||
|
import eu.kanade.presentation.browse.SourceFeedState
|
||||||
|
import eu.kanade.presentation.browse.SourceFeedStateImpl
|
||||||
|
import eu.kanade.presentation.browse.SourceFeedUI
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@ -44,12 +47,6 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||||
|
|
||||||
sealed class SourceFeed {
|
|
||||||
object Latest : SourceFeed()
|
|
||||||
object Browse : SourceFeed()
|
|
||||||
data class SourceSavedSearch(val feed: FeedSavedSearch, val savedSearch: SavedSearch) : SourceFeed()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of [SourceFeedController]
|
* Presenter of [SourceFeedController]
|
||||||
* Function calls should be done from here. UI calls should be done from the controller.
|
* Function calls should be done from here. UI calls should be done from the controller.
|
||||||
@ -59,6 +56,7 @@ sealed class SourceFeed {
|
|||||||
* @param preferences manages the preference calls.
|
* @param preferences manages the preference calls.
|
||||||
*/
|
*/
|
||||||
open class SourceFeedPresenter(
|
open class SourceFeedPresenter(
|
||||||
|
private val state: SourceFeedStateImpl = SourceFeedState() as SourceFeedStateImpl,
|
||||||
val source: CatalogueSource,
|
val source: CatalogueSource,
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
@ -71,7 +69,7 @@ open class SourceFeedPresenter(
|
|||||||
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
|
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
|
||||||
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
|
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
|
||||||
private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(),
|
private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(),
|
||||||
) : BasePresenter<SourceFeedController>() {
|
) : BasePresenter<SourceFeedController>(), SourceFeedState by state {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the different sources by user settings.
|
* Fetches the different sources by user settings.
|
||||||
@ -81,7 +79,7 @@ open class SourceFeedPresenter(
|
|||||||
/**
|
/**
|
||||||
* Subject which fetches image of given manga.
|
* Subject which fetches image of given manga.
|
||||||
*/
|
*/
|
||||||
private val fetchImageSubject = PublishSubject.create<Triple<List<Manga>, Source, SourceFeed>>()
|
private val fetchImageSubject = PublishSubject.create<Triple<List<Manga>, Source, SourceFeedUI>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription for fetching images of manga.
|
* Subscription for fetching images of manga.
|
||||||
@ -110,7 +108,10 @@ open class SourceFeedPresenter(
|
|||||||
|
|
||||||
getFeedSavedSearchBySourceId.subscribe(source.id)
|
getFeedSavedSearchBySourceId.subscribe(source.id)
|
||||||
.onEach {
|
.onEach {
|
||||||
getFeed(it)
|
val items = getSourcesToGetFeed(it)
|
||||||
|
state.items = items
|
||||||
|
state.isLoading = false
|
||||||
|
getFeed(items)
|
||||||
}
|
}
|
||||||
.launchIn(presenterScope)
|
.launchIn(presenterScope)
|
||||||
}
|
}
|
||||||
@ -148,54 +149,35 @@ open class SourceFeedPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSourcesToGetFeed(feedSavedSearch: List<FeedSavedSearch>): List<SourceFeed> {
|
private suspend fun getSourcesToGetFeed(feedSavedSearch: List<FeedSavedSearch>): List<SourceFeedUI> {
|
||||||
val savedSearches = getSavedSearchBySourceIdFeed.await(source.id)
|
val savedSearches = getSavedSearchBySourceIdFeed.await(source.id)
|
||||||
.associateBy { it.id }
|
.associateBy { it.id }
|
||||||
|
|
||||||
return listOfNotNull(
|
return listOfNotNull(
|
||||||
if (source.supportsLatest) {
|
if (source.supportsLatest) {
|
||||||
SourceFeed.Latest
|
SourceFeedUI.Latest(null)
|
||||||
} else null,
|
} else null,
|
||||||
SourceFeed.Browse,
|
SourceFeedUI.Browse(null),
|
||||||
) + feedSavedSearch
|
) + feedSavedSearch
|
||||||
.map { SourceFeed.SourceSavedSearch(it, savedSearches[it.savedSearch]!!) }
|
.map { SourceFeedUI.SourceSavedSearch(it, savedSearches[it.savedSearch]!!, null) }
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a catalogue search item
|
|
||||||
*/
|
|
||||||
protected open fun createCatalogueSearchItem(
|
|
||||||
sourceFeed: SourceFeed,
|
|
||||||
results: List<SourceFeedCardItem>?,
|
|
||||||
): SourceFeedItem {
|
|
||||||
return SourceFeedItem(sourceFeed, results)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiates get manga per feed.
|
* Initiates get manga per feed.
|
||||||
*/
|
*/
|
||||||
private suspend fun getFeed(feedSavedSearch: List<FeedSavedSearch>) {
|
private fun getFeed(feedSavedSearch: List<SourceFeedUI>) {
|
||||||
// Create image fetch subscription
|
// Create image fetch subscription
|
||||||
initializeFetchImageSubscription()
|
initializeFetchImageSubscription()
|
||||||
|
|
||||||
// Create items with the initial state
|
|
||||||
val initialItems = getSourcesToGetFeed(feedSavedSearch).map {
|
|
||||||
createCatalogueSearchItem(
|
|
||||||
it,
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var items = initialItems
|
|
||||||
|
|
||||||
fetchSourcesSubscription?.unsubscribe()
|
fetchSourcesSubscription?.unsubscribe()
|
||||||
fetchSourcesSubscription = Observable.from(getSourcesToGetFeed(feedSavedSearch))
|
fetchSourcesSubscription = Observable.from(feedSavedSearch)
|
||||||
.flatMap(
|
.flatMap(
|
||||||
{ sourceFeed ->
|
{ sourceFeed ->
|
||||||
Observable.defer {
|
Observable.defer {
|
||||||
when (sourceFeed) {
|
when (sourceFeed) {
|
||||||
SourceFeed.Browse -> source.fetchPopularManga(1)
|
is SourceFeedUI.Browse -> source.fetchPopularManga(1)
|
||||||
SourceFeed.Latest -> source.fetchLatestUpdates(1)
|
is SourceFeedUI.Latest -> source.fetchLatestUpdates(1)
|
||||||
is SourceFeed.SourceSavedSearch -> source.fetchSearchManga(
|
is SourceFeedUI.SourceSavedSearch -> source.fetchSearchManga(
|
||||||
page = 1,
|
page = 1,
|
||||||
query = sourceFeed.savedSearch.query.orEmpty(),
|
query = sourceFeed.savedSearch.query.orEmpty(),
|
||||||
filters = getFilterList(sourceFeed.savedSearch, source),
|
filters = getFilterList(sourceFeed.savedSearch, source),
|
||||||
@ -207,24 +189,21 @@ open class SourceFeedPresenter(
|
|||||||
.map { it.mangas } // Get manga from search result.
|
.map { it.mangas } // Get manga from search result.
|
||||||
.map { list -> list.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
.map { list -> list.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
||||||
.doOnNext { fetchImage(it, source, sourceFeed) } // Load manga covers.
|
.doOnNext { fetchImage(it, source, sourceFeed) } // Load manga covers.
|
||||||
.map { list -> createCatalogueSearchItem(sourceFeed, list.map { SourceFeedCardItem(it) }) }
|
.map { list -> sourceFeed.withResults(list.mapNotNull { it.toDomainManga() }) }
|
||||||
},
|
},
|
||||||
5,
|
5,
|
||||||
)
|
)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
// Update matching source with the obtained results
|
// Update matching source with the obtained results
|
||||||
.map { result ->
|
.doOnNext { result ->
|
||||||
items.map { item -> if (item.sourceFeed == result.sourceFeed) result else item }
|
synchronized(state) {
|
||||||
|
state.items = state.items?.map { item -> if (item.id == result.id) result else item }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Update current state
|
|
||||||
.doOnNext { items = it }
|
|
||||||
// Deliver initial state
|
// Deliver initial state
|
||||||
.startWith(initialItems)
|
.subscribe(
|
||||||
.subscribeLatestCache(
|
{},
|
||||||
{ view, manga ->
|
{ error ->
|
||||||
view.setItems(manga)
|
|
||||||
},
|
|
||||||
{ _, error ->
|
|
||||||
logcat(LogPriority.ERROR, error)
|
logcat(LogPriority.ERROR, error)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -249,7 +228,7 @@ open class SourceFeedPresenter(
|
|||||||
*
|
*
|
||||||
* @param manga the list of manga to initialize.
|
* @param manga the list of manga to initialize.
|
||||||
*/
|
*/
|
||||||
private fun fetchImage(manga: List<Manga>, source: Source, sourceFeed: SourceFeed) {
|
private fun fetchImage(manga: List<Manga>, source: Source, sourceFeed: SourceFeedUI) {
|
||||||
fetchImageSubject.onNext(Triple(manga, source, sourceFeed))
|
fetchImageSubject.onNext(Triple(manga, source, sourceFeed))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,8 +249,21 @@ open class SourceFeedPresenter(
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ (sourceFeed, manga) ->
|
{ (sourceFeed, manga) ->
|
||||||
@Suppress("DEPRECATION")
|
synchronized(state) {
|
||||||
view?.onMangaInitialized(sourceFeed, manga)
|
state.items = items?.map { itemUI ->
|
||||||
|
if (sourceFeed.id == itemUI.id) {
|
||||||
|
itemUI.withResults(
|
||||||
|
results = itemUI.results?.map {
|
||||||
|
if (it.id == manga.id) {
|
||||||
|
manga.toDomainManga()!!
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else itemUI
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ error ->
|
{ error ->
|
||||||
logcat(LogPriority.ERROR, error)
|
logcat(LogPriority.ERROR, error)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user