Search in history, favorites and local
This commit is contained in:
@@ -192,7 +192,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity"
|
||||
android:name="org.koitharu.kotatsu.search.ui.multi.SearchActivity"
|
||||
android:label="@string/search" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
|
||||
|
||||
@@ -39,6 +39,8 @@ fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
||||
|
||||
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
|
||||
|
||||
fun Collection<MangaWithTags>.toMangaList() = map { it.toManga() }
|
||||
|
||||
// Model to entity
|
||||
|
||||
fun Manga.toEntity() = MangaEntity(
|
||||
|
||||
@@ -23,7 +23,7 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||
import org.koitharu.kotatsu.search.ui.multi.SearchActivity
|
||||
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
||||
|
||||
class DetailsMenuProvider(
|
||||
@@ -92,7 +92,7 @@ class DetailsMenuProvider(
|
||||
|
||||
R.id.action_related -> {
|
||||
viewModel.manga.value?.let {
|
||||
activity.startActivity(MultiSearchActivity.newIntent(activity, it.title))
|
||||
activity.startActivity(SearchActivity.newIntent(activity, it.title))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,7 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
|
||||
RecyclerScrollKeeper(this).attach()
|
||||
}
|
||||
addMenuProvider(DownloadsMenuProvider(this, viewModel))
|
||||
viewModel.items.observe(this) {
|
||||
downloadsAdapter.items = it
|
||||
}
|
||||
viewModel.items.observe(this, downloadsAdapter)
|
||||
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.recyclerView))
|
||||
val menuInvalidator = MenuInvalidator(this)
|
||||
viewModel.hasActiveWorks.observe(this, menuInvalidator)
|
||||
|
||||
@@ -91,9 +91,7 @@ class ExploreFragment :
|
||||
checkNotNull(sourceSelectionController).attachToRecyclerView(this)
|
||||
}
|
||||
addMenuProvider(ExploreMenuProvider(binding.root.context))
|
||||
viewModel.content.observe(viewLifecycleOwner) {
|
||||
exploreAdapter?.items = it
|
||||
}
|
||||
viewModel.content.observe(viewLifecycleOwner, checkNotNull(exploreAdapter))
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||
viewModel.onOpenManga.observeEvent(viewLifecycleOwner, ::onOpenManga)
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
||||
|
||||
@@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.koitharu.kotatsu.core.db.MangaQueryBuilder
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||
@@ -32,6 +33,10 @@ abstract class FavouritesDao : MangaQueryBuilder.ConditionCallback {
|
||||
@Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit")
|
||||
abstract suspend fun findLast(limit: Int): List<FavouriteManga>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT manga.* FROM favourites LEFT JOIN manga ON manga.manga_id = favourites.manga_id WHERE favourites.deleted_at = 0 AND (manga.title LIKE :query OR manga.alt_title LIKE :query) LIMIT :limit")
|
||||
abstract suspend fun search(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
fun observeAll(
|
||||
order: ListSortOrder,
|
||||
filterOptions: Set<ListFilterOption>,
|
||||
|
||||
@@ -10,21 +10,21 @@ import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaList
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.toMangaSources
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||
import org.koitharu.kotatsu.core.util.ext.mapItems
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
||||
import org.koitharu.kotatsu.favourites.data.toManga
|
||||
import org.koitharu.kotatsu.favourites.data.toMangaList
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
@@ -43,12 +43,17 @@ class FavouritesRepository @Inject constructor(
|
||||
return entities.toMangaList()
|
||||
}
|
||||
|
||||
suspend fun search(query: String, limit: Int): List<Manga> {
|
||||
val entities = db.getFavouritesDao().search("%$query%", limit)
|
||||
return entities.toMangaList().sortedBy { it.title.levenshteinDistance(query) }
|
||||
}
|
||||
|
||||
fun observeAll(order: ListSortOrder, filterOptions: Set<ListFilterOption>, limit: Int): Flow<List<Manga>> {
|
||||
if (ListFilterOption.Downloaded in filterOptions) {
|
||||
return localObserver.observeAll(order, filterOptions, limit)
|
||||
}
|
||||
return db.getFavouritesDao().observeAll(order, filterOptions, limit)
|
||||
.mapItems { it.toManga() }
|
||||
.map { it.toMangaList() }
|
||||
}
|
||||
|
||||
suspend fun getManga(categoryId: Long): List<Manga> {
|
||||
@@ -66,7 +71,7 @@ class FavouritesRepository @Inject constructor(
|
||||
return localObserver.observeAll(categoryId, order, filterOptions, limit)
|
||||
}
|
||||
return db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit)
|
||||
.mapItems { it.toManga() }
|
||||
.map { it.toMangaList() }
|
||||
}
|
||||
|
||||
fun observeAll(categoryId: Long, filterOptions: Set<ListFilterOption>, limit: Int): Flow<List<Manga>> {
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.db.MangaQueryBuilder
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||
@@ -23,6 +24,10 @@ abstract class HistoryDao : MangaQueryBuilder.ConditionCallback {
|
||||
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT manga.* FROM history LEFT JOIN manga ON manga.manga_id = history.manga_id WHERE history.deleted_at = 0 AND (manga.title LIKE :query OR manga.alt_title LIKE :query) LIMIT :limit")
|
||||
abstract suspend fun search(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC")
|
||||
abstract fun observeAll(): Flow<List<HistoryWithManga>>
|
||||
|
||||
@@ -10,11 +10,10 @@ import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTag
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaList
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTagsList
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.findById
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
@@ -31,6 +30,7 @@ import org.koitharu.kotatsu.list.domain.ReadingProgress
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble
|
||||
import org.koitharu.kotatsu.tracker.domain.CheckNewChaptersUseCase
|
||||
@@ -52,6 +52,11 @@ class HistoryRepository @Inject constructor(
|
||||
return entities.map { it.manga.toManga(it.tags.toMangaTags()) }
|
||||
}
|
||||
|
||||
suspend fun search(query: String, limit: Int): List<Manga> {
|
||||
val entities = db.getHistoryDao().search("%$query%", limit)
|
||||
return entities.toMangaList().sortedBy { it.title.levenshteinDistance(query) }
|
||||
}
|
||||
|
||||
suspend fun getCount(): Int {
|
||||
return db.getHistoryDao().getCount()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
@@ -62,7 +63,12 @@ class LocalMangaRepository @Inject constructor(
|
||||
isSearchWithFiltersSupported = true,
|
||||
)
|
||||
|
||||
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.RATING, SortOrder.NEWEST)
|
||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.ALPHABETICAL,
|
||||
SortOrder.RATING,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.RELEVANCE,
|
||||
)
|
||||
|
||||
override var defaultSortOrder: SortOrder
|
||||
get() = settings.localListOrder
|
||||
@@ -102,6 +108,9 @@ class LocalMangaRepository @Inject constructor(
|
||||
val isNsfw = contentRating == ContentRating.ADULT
|
||||
list.retainAll { it.manga.isNsfw == isNsfw }
|
||||
}
|
||||
if (!query.isNullOrEmpty() && order == SortOrder.RELEVANCE) {
|
||||
list.sortBy { it.manga.title.levenshteinDistance(query) }
|
||||
}
|
||||
}
|
||||
when (order) {
|
||||
SortOrder.ALPHABETICAL -> list.sortWith(compareBy(AlphanumComparator()) { x -> x.manga.title })
|
||||
|
||||
@@ -66,7 +66,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||
import org.koitharu.kotatsu.search.ui.multi.SearchActivity
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||
@@ -258,7 +258,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
override fun onQueryClick(query: String, submit: Boolean) {
|
||||
viewBinding.searchView.query = query
|
||||
if (submit && query.isNotEmpty()) {
|
||||
startActivity(MultiSearchActivity.newIntent(this, query))
|
||||
startActivity(SearchActivity.newIntent(this, query))
|
||||
searchSuggestionViewModel.saveQuery(query)
|
||||
viewBinding.searchView.post {
|
||||
closeSearchCallback.handleOnBackPressed()
|
||||
|
||||
@@ -54,7 +54,7 @@ class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
|
||||
}
|
||||
viewBinding.imageViewAvatar.setOnClickListener(this)
|
||||
|
||||
viewModel.content.observe(this, listAdapter::setItems)
|
||||
viewModel.content.observe(this, listAdapter)
|
||||
viewModel.user.observe(this, this::onUserChanged)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
|
||||
|
||||
@@ -23,7 +23,7 @@ import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding
|
||||
import org.koitharu.kotatsu.databinding.ActivitySearchBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||
@@ -38,12 +38,12 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter
|
||||
import org.koitharu.kotatsu.search.ui.multi.adapter.SearchAdapter
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MultiSearchActivity :
|
||||
BaseActivity<ActivitySearchMultiBinding>(),
|
||||
class SearchActivity :
|
||||
BaseActivity<ActivitySearchBinding>(),
|
||||
MangaListListener,
|
||||
ListSelectionController.Callback {
|
||||
|
||||
@@ -53,16 +53,15 @@ class MultiSearchActivity :
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
private val viewModel by viewModels<MultiSearchViewModel>()
|
||||
private lateinit var adapter: MultiSearchAdapter
|
||||
private val viewModel by viewModels<SearchViewModel>()
|
||||
private lateinit var selectionController: ListSelectionController
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivitySearchMultiBinding.inflate(layoutInflater))
|
||||
setContentView(ActivitySearchBinding.inflate(layoutInflater))
|
||||
title = viewModel.query
|
||||
|
||||
val itemCLickListener = OnListItemClickListener<MultiSearchListModel> { item, view ->
|
||||
val itemCLickListener = OnListItemClickListener<SearchResultsListModel> { item, view ->
|
||||
startActivity(
|
||||
MangaListActivity.newIntent(
|
||||
view.context,
|
||||
@@ -79,7 +78,7 @@ class MultiSearchActivity :
|
||||
registryOwner = this,
|
||||
callback = this,
|
||||
)
|
||||
adapter = MultiSearchAdapter(
|
||||
val adapter = SearchAdapter(
|
||||
lifecycleOwner = this,
|
||||
coil = coil,
|
||||
listener = this,
|
||||
@@ -96,7 +95,7 @@ class MultiSearchActivity :
|
||||
setSubtitle(R.string.search_results)
|
||||
}
|
||||
|
||||
viewModel.list.observe(this) { adapter.items = it }
|
||||
viewModel.list.observe(this, adapter)
|
||||
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
|
||||
viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.recyclerView))
|
||||
}
|
||||
@@ -194,7 +193,7 @@ class MultiSearchActivity :
|
||||
const val EXTRA_QUERY = "query"
|
||||
|
||||
fun newIntent(context: Context, query: String) =
|
||||
Intent(context, MultiSearchActivity::class.java)
|
||||
Intent(context, SearchActivity::class.java)
|
||||
.putExtra(EXTRA_QUERY, query)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,33 @@
|
||||
package org.koitharu.kotatsu.search.ui.multi
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
data class MultiSearchListModel(
|
||||
data class SearchResultsListModel(
|
||||
@StringRes val titleResId: Int,
|
||||
val source: MangaSource,
|
||||
val hasMore: Boolean,
|
||||
val list: List<MangaListModel>,
|
||||
val error: Throwable?,
|
||||
) : ListModel {
|
||||
|
||||
fun getTitle(context: Context): String = if (titleResId != 0) {
|
||||
context.getString(titleResId)
|
||||
} else {
|
||||
source.getTitle(context)
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is MultiSearchListModel && source == other.source
|
||||
return other is SearchResultsListModel && source == other.source && titleResId == other.titleResId
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
return if (previousState is MultiSearchListModel && previousState.list != list) {
|
||||
return if (previousState is SearchResultsListModel && previousState.list != list) {
|
||||
ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED
|
||||
} else {
|
||||
super.getChangePayload(previousState)
|
||||
@@ -23,6 +23,8 @@ import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.model.UnknownMangaSource
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
@@ -31,13 +33,17 @@ import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -45,16 +51,19 @@ private const val MAX_PARALLELISM = 4
|
||||
private const val MIN_HAS_MORE_ITEMS = 8
|
||||
|
||||
@HiltViewModel
|
||||
class MultiSearchViewModel @Inject constructor(
|
||||
class SearchViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
val query = savedStateHandle.get<String>(MultiSearchActivity.EXTRA_QUERY).orEmpty()
|
||||
val query = savedStateHandle.get<String>(SearchActivity.EXTRA_QUERY).orEmpty()
|
||||
|
||||
private val retryCounter = MutableStateFlow(0)
|
||||
private val listData = retryCounter.flatMapLatest {
|
||||
@@ -108,7 +117,10 @@ class MultiSearchViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
private fun searchImpl(q: String): Flow<List<MultiSearchListModel>> = channelFlow {
|
||||
private fun searchImpl(q: String): Flow<List<SearchResultsListModel>> = channelFlow {
|
||||
searchHistory(q)?.let { send(it) }
|
||||
searchFavorites(q)?.let { send(it) }
|
||||
searchLocal(q)?.let { send(it) }
|
||||
val sources = sourcesRepository.getEnabledSources()
|
||||
if (sources.isEmpty()) {
|
||||
return@channelFlow
|
||||
@@ -132,12 +144,12 @@ class MultiSearchViewModel @Inject constructor(
|
||||
if (list.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list, null)
|
||||
SearchResultsListModel(0, source, list.size > MIN_HAS_MORE_ITEMS, list, null)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
error.printStackTraceDebug()
|
||||
MultiSearchListModel(source, true, emptyList(), error)
|
||||
SearchResultsListModel(0, source, true, emptyList(), error)
|
||||
},
|
||||
)
|
||||
if (item != null) {
|
||||
@@ -146,7 +158,94 @@ class MultiSearchViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}.joinAll()
|
||||
}.runningFold<MultiSearchListModel, List<MultiSearchListModel>?>(null) { list, item -> list.orEmpty() + item }
|
||||
}.runningFold<SearchResultsListModel, List<SearchResultsListModel>?>(null) { list, item -> list.orEmpty() + item }
|
||||
.filterNotNull()
|
||||
.onEmpty { emit(emptyList()) }
|
||||
|
||||
private suspend fun searchHistory(q: String): SearchResultsListModel? {
|
||||
return runCatchingCancellable {
|
||||
historyRepository.search(q, Int.MAX_VALUE)
|
||||
}.fold(
|
||||
onSuccess = { result ->
|
||||
if (result.isNotEmpty()) {
|
||||
SearchResultsListModel(
|
||||
titleResId = R.string.history,
|
||||
source = UnknownMangaSource,
|
||||
hasMore = false,
|
||||
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
|
||||
error = null,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
SearchResultsListModel(
|
||||
titleResId = R.string.history,
|
||||
source = UnknownMangaSource,
|
||||
hasMore = false,
|
||||
list = emptyList(),
|
||||
error = error,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun searchFavorites(q: String): SearchResultsListModel? {
|
||||
return runCatchingCancellable {
|
||||
favouritesRepository.search(q, Int.MAX_VALUE)
|
||||
}.fold(
|
||||
onSuccess = { result ->
|
||||
if (result.isNotEmpty()) {
|
||||
SearchResultsListModel(
|
||||
titleResId = R.string.favourites,
|
||||
source = UnknownMangaSource,
|
||||
hasMore = false,
|
||||
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
|
||||
error = null,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
SearchResultsListModel(
|
||||
titleResId = R.string.favourites,
|
||||
source = UnknownMangaSource,
|
||||
hasMore = false,
|
||||
list = emptyList(),
|
||||
error = error,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun searchLocal(q: String): SearchResultsListModel? {
|
||||
return runCatchingCancellable {
|
||||
localMangaRepository.getList(0, SortOrder.RELEVANCE, MangaListFilter(query = q))
|
||||
}.fold(
|
||||
onSuccess = { result ->
|
||||
if (result.isNotEmpty()) {
|
||||
SearchResultsListModel(
|
||||
titleResId = 0,
|
||||
source = LocalMangaSource,
|
||||
hasMore = result.size > MIN_HAS_MORE_ITEMS,
|
||||
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
|
||||
error = null,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
SearchResultsListModel(
|
||||
titleResId = 0,
|
||||
source = LocalMangaSource,
|
||||
hasMore = true,
|
||||
list = emptyList(),
|
||||
error = error,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.koitharu.kotatsu.search.ui.multi.adapter
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
|
||||
import coil.ImageLoader
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||
@@ -14,16 +16,16 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel
|
||||
import org.koitharu.kotatsu.search.ui.multi.SearchResultsListModel
|
||||
|
||||
class MultiSearchAdapter(
|
||||
class SearchAdapter(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
coil: ImageLoader,
|
||||
listener: MangaListListener,
|
||||
itemClickListener: OnListItemClickListener<MultiSearchListModel>,
|
||||
itemClickListener: OnListItemClickListener<SearchResultsListModel>,
|
||||
sizeResolver: ItemSizeResolver,
|
||||
selectionDecoration: MangaSelectionDecoration,
|
||||
) : BaseListAdapter<ListModel>() {
|
||||
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
|
||||
|
||||
init {
|
||||
val pool = RecycledViewPool()
|
||||
@@ -44,4 +46,8 @@ class MultiSearchAdapter(
|
||||
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
|
||||
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener))
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
return (items.getOrNull(position) as? SearchResultsListModel)?.getTitle(context)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||
@@ -20,7 +19,7 @@ import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel
|
||||
import org.koitharu.kotatsu.search.ui.multi.SearchResultsListModel
|
||||
|
||||
fun searchResultsAD(
|
||||
sharedPool: RecycledViewPool,
|
||||
@@ -29,8 +28,8 @@ fun searchResultsAD(
|
||||
sizeResolver: ItemSizeResolver,
|
||||
selectionDecoration: MangaSelectionDecoration,
|
||||
listener: OnListItemClickListener<Manga>,
|
||||
itemClickListener: OnListItemClickListener<MultiSearchListModel>,
|
||||
) = adapterDelegateViewBinding<MultiSearchListModel, ListModel, ItemListGroupBinding>(
|
||||
itemClickListener: OnListItemClickListener<SearchResultsListModel>,
|
||||
) = adapterDelegateViewBinding<SearchResultsListModel, ListModel, ItemListGroupBinding>(
|
||||
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
@@ -40,13 +39,13 @@ fun searchResultsAD(
|
||||
)
|
||||
binding.recyclerView.addItemDecoration(selectionDecoration)
|
||||
binding.recyclerView.adapter = adapter
|
||||
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing)
|
||||
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
|
||||
val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener)
|
||||
binding.buttonMore.setOnClickListener(eventListener)
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = item.source.getTitle(context)
|
||||
binding.textViewTitle.text = item.getTitle(context)
|
||||
binding.buttonMore.isVisible = item.hasMore
|
||||
adapter.items = item.list
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
@@ -31,7 +31,7 @@ class TrackerCategoriesConfigSheet :
|
||||
val adapter = TrackerCategoriesConfigAdapter(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
|
||||
viewModel.content.observe(viewLifecycleOwner, adapter)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: FavouriteCategory, view: View) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@@ -28,7 +30,6 @@ import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -48,8 +49,6 @@ class FeedFragment :
|
||||
|
||||
private val viewModel by viewModels<FeedViewModel>()
|
||||
|
||||
private var feedAdapter: FeedAdapter? = null
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -58,11 +57,12 @@ class FeedFragment :
|
||||
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
val sizeResolver = StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width))
|
||||
feedAdapter = FeedAdapter(coil, viewLifecycleOwner, this, sizeResolver) { item, v ->
|
||||
val feedAdapter = FeedAdapter(coil, viewLifecycleOwner, this, sizeResolver) { item, v ->
|
||||
viewModel.onItemClick(item)
|
||||
onItemClick(item.manga, v)
|
||||
}
|
||||
with(binding.recyclerView) {
|
||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
adapter = feedAdapter
|
||||
setHasFixedSize(true)
|
||||
addOnScrollListener(PaginationScrollListener(4, this@FeedFragment))
|
||||
@@ -73,17 +73,12 @@ class FeedFragment :
|
||||
addMenuProvider(FeedMenuProvider(binding.recyclerView, viewModel))
|
||||
|
||||
viewModel.isHeaderEnabled.drop(1).observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
|
||||
viewModel.content.observe(viewLifecycleOwner, this::onListChanged)
|
||||
viewModel.content.observe(viewLifecycleOwner, feedAdapter)
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||
viewModel.onFeedCleared.observeEvent(viewLifecycleOwner) { onFeedCleared() }
|
||||
viewModel.isRunning.observe(viewLifecycleOwner, this::onIsTrackerRunningChanged)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
feedAdapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
val rv = requireViewBinding().recyclerView
|
||||
rv.updatePadding(
|
||||
@@ -112,10 +107,6 @@ class FeedFragment :
|
||||
context.startActivity(UpdatesActivity.newIntent(context))
|
||||
}
|
||||
|
||||
private fun onListChanged(list: List<ListModel>) {
|
||||
feedAdapter?.items = list
|
||||
}
|
||||
|
||||
private fun onFeedCleared() {
|
||||
val snackbar = Snackbar.make(
|
||||
requireViewBinding().recyclerView,
|
||||
|
||||
@@ -56,7 +56,7 @@ class ShelfWidgetConfigActivity :
|
||||
viewModel.checkedId = config.categoryId
|
||||
viewBinding.switchBackground.isChecked = config.hasBackground
|
||||
|
||||
viewModel.content.observe(this, this::onContentChanged)
|
||||
viewModel.content.observe(this, adapter)
|
||||
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
|
||||
}
|
||||
|
||||
@@ -96,10 +96,6 @@ class ShelfWidgetConfigActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContentChanged(categories: List<CategoryItem>) {
|
||||
adapter.items = categories
|
||||
}
|
||||
|
||||
private fun updateWidget() {
|
||||
val intent = Intent(this, ShelfWidgetProvider::class.java)
|
||||
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
|
||||
android:id="@id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -19,7 +19,6 @@
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/list_spacing_normal"
|
||||
app:bubbleSize="small"
|
||||
tools:layoutManager="org.koitharu.kotatsu.core.ui.list.FitHeightLinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
<item name="bubbleColor">?colorTertiary</item>
|
||||
<item name="bubbleTextColor">?colorOnTertiary</item>
|
||||
<item name="trackColor">?colorOutline</item>
|
||||
<item name="bubbleSize">normal</item>
|
||||
<item name="bubbleSize">small</item>
|
||||
<item name="scrollerOffset">@dimen/grid_spacing_outer</item>
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user