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