Search in history, favorites and local

This commit is contained in:
Koitharu
2024-09-24 11:13:09 +03:00
parent a7a0a7f0db
commit a87ef0a0a6
23 changed files with 198 additions and 72 deletions

View File

@@ -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"

View File

@@ -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(

View File

@@ -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))
}
}

View File

@@ -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)

View File

@@ -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))

View File

@@ -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>,

View File

@@ -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>> {

View File

@@ -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>>

View File

@@ -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()
}

View File

@@ -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 })

View File

@@ -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()

View File

@@ -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))

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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,
)
},
)
}
}

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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

View File

@@ -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"

View File

@@ -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" />

View File

@@ -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>