diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e26066b48..cd7988db3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -192,7 +192,7 @@
) = Manga(
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
+fun Collection.toMangaList() = map { it.toManga() }
+
// Model to entity
fun Manga.toEntity() = MangaEntity(
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
index c37f5813e..0cc332f30 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
@@ -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))
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt
index f798498c7..42d7eb43a 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt
@@ -58,9 +58,7 @@ class DownloadsActivity : BaseActivity(),
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)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt
index 2f4ea0c24..8c71cb5d5 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt
@@ -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))
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
index 8e0443d6d..fd8376682 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
@@ -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
+ @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
+
fun observeAll(
order: ListSortOrder,
filterOptions: Set,
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
index eed5c14e7..c65a2acce 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
@@ -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 {
+ val entities = db.getFavouritesDao().search("%$query%", limit)
+ return entities.toMangaList().sortedBy { it.title.levenshteinDistance(query) }
+ }
+
fun observeAll(order: ListSortOrder, filterOptions: Set, limit: Int): Flow> {
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 {
@@ -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, limit: Int): Flow> {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt
index c0c9b9bd0..1065ee922 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt
@@ -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
+ @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
+
@Transaction
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC")
abstract fun observeAll(): Flow>
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt
index 6ffe83458..e0879081b 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt
@@ -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 {
+ val entities = db.getHistoryDao().search("%$query%", limit)
+ return entities.toMangaList().sortedBy { it.title.levenshteinDistance(query) }
+ }
+
suspend fun getCount(): Int {
return db.getHistoryDao().getCount()
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt
index c27deb7ef..f2c6ae588 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt
@@ -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 = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.RATING, SortOrder.NEWEST)
+ override val sortOrders: Set = 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 })
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt
index afd073a9b..384fbd453 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt
@@ -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(), 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()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt
index 07badad9b..3f4d6bafa 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt
@@ -54,7 +54,7 @@ class ScrobblerConfigActivity : BaseActivity(),
}
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))
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt
similarity index 90%
rename from app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt
index f4975fa04..dc72807e8 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt
@@ -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(),
+class SearchActivity :
+ BaseActivity(),
MangaListListener,
ListSelectionController.Callback {
@@ -53,16 +53,15 @@ class MultiSearchActivity :
@Inject
lateinit var settings: AppSettings
- private val viewModel by viewModels()
- private lateinit var adapter: MultiSearchAdapter
+ private val viewModel by viewModels()
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 { item, view ->
+ val itemCLickListener = OnListItemClickListener { 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)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchResultsListModel.kt
similarity index 55%
rename from app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchResultsListModel.kt
index 317652294..ef6a4d5ee 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchResultsListModel.kt
@@ -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,
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)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt
similarity index 58%
rename from app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt
index 155c9eefd..3edd17e9e 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt
@@ -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()
- val query = savedStateHandle.get(MultiSearchActivity.EXTRA_QUERY).orEmpty()
+ val query = savedStateHandle.get(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> = channelFlow {
+ private fun searchImpl(q: String): Flow> = 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?>(null) { list, item -> list.orEmpty() + item }
+ }.runningFold?>(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,
+ )
+ },
+ )
+ }
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchAdapter.kt
similarity index 77%
rename from app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchAdapter.kt
index 607c525ee..1d8e62937 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchAdapter.kt
@@ -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,
+ itemClickListener: OnListItemClickListener,
sizeResolver: ItemSizeResolver,
selectionDecoration: MangaSelectionDecoration,
-) : BaseListAdapter() {
+) : BaseListAdapter(), 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)
+ }
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt
index 9363051e8..64a45b8d8 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt
@@ -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,
- itemClickListener: OnListItemClickListener,
-) = adapterDelegateViewBinding(
+ itemClickListener: OnListItemClickListener,
+) = adapterDelegateViewBinding(
{ 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()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt
index c9ad94181..3e0a07e34 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt
@@ -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) {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt
index 4643610f1..9c03bbd0f 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt
@@ -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()
- 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) {
- feedAdapter?.items = list
- }
-
private fun onFeedCleared() {
val snackbar = Snackbar.make(
requireViewBinding().recyclerView,
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetConfigActivity.kt
index aee292134..f3685c47d 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetConfigActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetConfigActivity.kt
@@ -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) {
- adapter.items = categories
- }
-
private fun updateWidget() {
val intent = Intent(this, ShelfWidgetProvider::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
diff --git a/app/src/main/res/layout/activity_search_multi.xml b/app/src/main/res/layout/activity_search.xml
similarity index 95%
rename from app/src/main/res/layout/activity_search_multi.xml
rename to app/src/main/res/layout/activity_search.xml
index 7d741d592..c481d4599 100644
--- a/app/src/main/res/layout/activity_search_multi.xml
+++ b/app/src/main/res/layout/activity_search.xml
@@ -27,7 +27,7 @@
-
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 4cac8d89f..177ba4fe0 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -153,7 +153,7 @@
- ?colorTertiary
- ?colorOnTertiary
- ?colorOutline
- - normal
+ - small
- @dimen/grid_spacing_outer