Suggest tags on search
This commit is contained in:
@@ -50,6 +50,8 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
||||||
android:label="@string/search" />
|
android:label="@string/search" />
|
||||||
|
<activity android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
||||||
|
android:label="@string/search_manga" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
||||||
android:label="@string/settings" />
|
android:label="@string/settings" />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -46,15 +45,6 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsLi
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getTitle(): CharSequence? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
getTitle()?.let {
|
|
||||||
activity?.title = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
|
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||||
val newInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val newInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
if (newInsets != lastInsets) {
|
if (newInsets != lastInsets) {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
||||||
chip.setEnsureMinTouchTargetSize(false)
|
chip.setEnsureMinTouchTargetSize(false)
|
||||||
chip.setOnClickListener(chipOnClickListener)
|
chip.setOnClickListener(chipOnClickListener)
|
||||||
|
chip.isCheckable = false
|
||||||
addView(chip)
|
addView(chip)
|
||||||
return chip
|
return chip
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,45 @@ abstract class TagsDao {
|
|||||||
@Query("SELECT * FROM tags WHERE source = :source")
|
@Query("SELECT * FROM tags WHERE source = :source")
|
||||||
abstract suspend fun findTags(source: String): List<TagEntity>
|
abstract suspend fun findTags(source: String): List<TagEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""SELECT tags.* FROM tags
|
||||||
|
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
||||||
|
GROUP BY manga_tags.tag_id
|
||||||
|
ORDER BY COUNT(manga_id) DESC
|
||||||
|
LIMIT :limit"""
|
||||||
|
)
|
||||||
|
abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""SELECT tags.* FROM tags
|
||||||
|
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
||||||
|
WHERE tags.source = :source
|
||||||
|
GROUP BY manga_tags.tag_id
|
||||||
|
ORDER BY COUNT(manga_id) DESC
|
||||||
|
LIMIT :limit"""
|
||||||
|
)
|
||||||
|
abstract suspend fun findPopularTags(source: String, limit: Int): List<TagEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""SELECT tags.* FROM tags
|
||||||
|
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
||||||
|
WHERE tags.source = :source AND title LIKE :query
|
||||||
|
GROUP BY manga_tags.tag_id
|
||||||
|
ORDER BY COUNT(manga_id) DESC
|
||||||
|
LIMIT :limit"""
|
||||||
|
)
|
||||||
|
abstract suspend fun findTags(source: String, query: String, limit: Int): List<TagEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""SELECT tags.* FROM tags
|
||||||
|
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
||||||
|
WHERE title LIKE :query
|
||||||
|
GROUP BY manga_tags.tag_id
|
||||||
|
ORDER BY COUNT(manga_id) DESC
|
||||||
|
LIMIT :limit"""
|
||||||
|
)
|
||||||
|
abstract suspend fun findTags(query: String, limit: Int): List<TagEntity>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
abstract suspend fun insert(tag: TagEntity): Long
|
abstract suspend fun insert(tag: TagEntity): Long
|
||||||
|
|
||||||
|
|||||||
@@ -16,27 +16,26 @@ import androidx.core.view.updatePadding
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.util.CoilUtils
|
import coil.util.CoilUtils
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.*
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaState
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
|
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
|
||||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||||
import org.koitharu.kotatsu.utils.FileSize
|
import org.koitharu.kotatsu.utils.FileSize
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.*
|
||||||
|
|
||||||
class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickListener,
|
class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickListener,
|
||||||
View.OnLongClickListener {
|
View.OnLongClickListener, ChipsView.OnChipClickListener {
|
||||||
|
|
||||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||||
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE)
|
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE)
|
||||||
@@ -54,6 +53,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
|||||||
binding.buttonRead.setOnLongClickListener(this)
|
binding.buttonRead.setOnLongClickListener(this)
|
||||||
binding.imageViewCover.setOnClickListener(this)
|
binding.imageViewCover.setOnClickListener(this)
|
||||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
binding.chipsTags.onChipClickListener = this
|
||||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||||
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
|
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
|
||||||
@@ -231,6 +231,11 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onChipClick(chip: Chip, data: Any?) {
|
||||||
|
val tag = data as? MangaTag ?: return
|
||||||
|
startActivity(MangaListActivity.newIntent(requireContext(), tag))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
binding.root.updatePadding(
|
binding.root.updatePadding(
|
||||||
left = insets.left,
|
left = insets.left,
|
||||||
@@ -244,7 +249,8 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
|||||||
manga.tags.map { tag ->
|
manga.tags.map { tag ->
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
icon = 0
|
icon = 0,
|
||||||
|
data = tag,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -103,10 +103,6 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
|
|||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): CharSequence? {
|
|
||||||
return context?.getString(R.string.favourites)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onError(e: Throwable) {
|
private fun onError(e: Throwable) {
|
||||||
Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
|
Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ class HistoryListFragment : MangaListFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): CharSequence? {
|
|
||||||
return context?.getString(R.string.history)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) {
|
override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) {
|
||||||
super.onCreatePopupMenu(inflater, menu, data)
|
super.onCreatePopupMenu(inflater, menu, data)
|
||||||
inflater.inflate(R.menu.popup_history, menu)
|
inflater.inflate(R.menu.popup_history, menu)
|
||||||
|
|||||||
@@ -92,10 +92,6 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): CharSequence? {
|
|
||||||
return context?.getString(R.string.local_storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(result: List<@JvmSuppressWildcards Uri>) {
|
override fun onActivityResult(result: List<@JvmSuppressWildcards Uri>) {
|
||||||
if (result.isEmpty()) return
|
if (result.isEmpty()) return
|
||||||
viewModel.importFiles(result)
|
viewModel.importFiles(result)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSection
|
import org.koitharu.kotatsu.core.prefs.AppSection
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||||
@@ -42,6 +43,7 @@ import org.koitharu.kotatsu.local.ui.LocalListFragment
|
|||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||||
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
|
import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
|
||||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
|
||||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||||
@@ -258,6 +260,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTagClick(tag: MangaTag) {
|
||||||
|
startActivity(
|
||||||
|
MangaListActivity.newIntent(this, tag)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onQueryChanged(query: String) {
|
override fun onQueryChanged(query: String) {
|
||||||
searchSuggestionViewModel.onQueryChanged(query)
|
searchSuggestionViewModel.onQueryChanged(query)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ class RemoteListFragment : MangaListFragment() {
|
|||||||
viewModel.loadNextPage()
|
viewModel.loadNextPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): CharSequence {
|
|
||||||
return source.title
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
inflater.inflate(R.menu.opt_list_remote, menu)
|
inflater.inflate(R.menu.opt_list_remote, menu)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
import org.koitharu.kotatsu.core.model.SortOrder
|
import org.koitharu.kotatsu.core.model.SortOrder
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
@@ -79,6 +80,17 @@ class MangaSearchRepository(
|
|||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getTagsSuggestion(query: String, limit: Int, source: MangaSource?): List<MangaTag> {
|
||||||
|
return when {
|
||||||
|
query.isNotEmpty() && source != null -> db.tagsDao.findTags(source.name, "%$query%", limit)
|
||||||
|
query.isNotEmpty() -> db.tagsDao.findTags("%$query%", limit)
|
||||||
|
source != null -> db.tagsDao.findTags(source.name, limit)
|
||||||
|
else -> db.tagsDao.findPopularTags(limit)
|
||||||
|
}.map {
|
||||||
|
it.toMangaTag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun saveSearchQuery(query: String) {
|
fun saveSearchQuery(query: String) {
|
||||||
recentSuggestions.saveRecentQuery(query, null)
|
recentSuggestions.saveRecentQuery(query, null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.getViewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivitySearchGlobalBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.filter.FilterState
|
||||||
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||||
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
||||||
|
|
||||||
|
class MangaListActivity : BaseActivity<ActivitySearchGlobalBinding>() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(ActivitySearchGlobalBinding.inflate(layoutInflater))
|
||||||
|
val tag = intent.getParcelableExtra<MangaTag>(EXTRA_TAG) ?: run {
|
||||||
|
finishAfterTransition()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
val fm = supportFragmentManager
|
||||||
|
if (fm.findFragmentById(R.id.container) == null) {
|
||||||
|
fm.commit {
|
||||||
|
val fragment = RemoteListFragment.newInstance(tag.source)
|
||||||
|
replace(R.id.container, fragment)
|
||||||
|
runOnCommit(ApplyFilterRunnable(fragment, tag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
with(binding.toolbar) {
|
||||||
|
updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right
|
||||||
|
)
|
||||||
|
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
topMargin = insets.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.container.updatePadding(
|
||||||
|
bottom = insets.bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ApplyFilterRunnable(
|
||||||
|
private val fragment: Fragment,
|
||||||
|
private val tag: MangaTag,
|
||||||
|
) : Runnable {
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
val viewModel = fragment.getViewModel<RemoteListViewModel> {
|
||||||
|
parametersOf(tag.source)
|
||||||
|
}
|
||||||
|
viewModel.applyFilter(FilterState(viewModel.filter.sortOrder, setOf(tag)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val EXTRA_TAG = "tag"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, tag: MangaTag) =
|
||||||
|
Intent(context, MangaListActivity::class.java)
|
||||||
|
.putExtra(EXTRA_TAG, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,8 +21,6 @@ class SearchFragment : MangaListFragment() {
|
|||||||
viewModel.loadNextPage()
|
viewModel.loadNextPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle() = query
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ARG_QUERY = "query"
|
private const val ARG_QUERY = "query"
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ class GlobalSearchFragment : MangaListFragment() {
|
|||||||
|
|
||||||
override fun onScrolledToEnd() = Unit
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|
||||||
override fun getTitle(): CharSequence? {
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ARG_QUERY = "query"
|
private const val ARG_QUERY = "query"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.search.ui.suggestion
|
package org.koitharu.kotatsu.search.ui.suggestion
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
|
||||||
interface SearchSuggestionListener {
|
interface SearchSuggestionListener {
|
||||||
|
|
||||||
@@ -11,4 +12,6 @@ interface SearchSuggestionListener {
|
|||||||
fun onQueryChanged(query: String)
|
fun onQueryChanged(query: String)
|
||||||
|
|
||||||
fun onClearSearchHistory()
|
fun onClearSearchHistory()
|
||||||
|
|
||||||
|
fun onTagClick(tag: MangaTag)
|
||||||
}
|
}
|
||||||
@@ -7,15 +7,18 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
|
||||||
private const val DEBOUNCE_TIMEOUT = 500L
|
private const val DEBOUNCE_TIMEOUT = 500L
|
||||||
private const val SEARCH_THRESHOLD = 3
|
private const val SEARCH_THRESHOLD = 3
|
||||||
private const val MAX_MANGA_ITEMS = 3
|
private const val MAX_MANGA_ITEMS = 3
|
||||||
private const val MAX_QUERY_ITEMS = 120
|
private const val MAX_QUERY_ITEMS = 16
|
||||||
private const val MAX_SUGGESTION_ITEMS = MAX_MANGA_ITEMS + MAX_QUERY_ITEMS + 1
|
private const val MAX_TAGS_ITEMS = 8
|
||||||
|
private const val MAX_SUGGESTION_ITEMS = MAX_MANGA_ITEMS + MAX_QUERY_ITEMS + 2
|
||||||
|
|
||||||
class SearchSuggestionViewModel(
|
class SearchSuggestionViewModel(
|
||||||
private val repository: MangaSearchRepository,
|
private val repository: MangaSearchRepository,
|
||||||
@@ -77,6 +80,10 @@ class SearchSuggestionViewModel(
|
|||||||
if (src != null) {
|
if (src != null) {
|
||||||
result += SearchSuggestionItem.Header(src, isLocalSearch)
|
result += SearchSuggestionItem.Header(src, isLocalSearch)
|
||||||
}
|
}
|
||||||
|
val tags = repository.getTagsSuggestion(q, MAX_TAGS_ITEMS, src.takeIf { srcOnly })
|
||||||
|
if (tags.isNotEmpty()) {
|
||||||
|
result.add(SearchSuggestionItem.Tags(mapTags(tags)))
|
||||||
|
}
|
||||||
if (q.length >= SEARCH_THRESHOLD) {
|
if (q.length >= SEARCH_THRESHOLD) {
|
||||||
repository.getMangaSuggestion(q, MAX_MANGA_ITEMS, src.takeIf { srcOnly })
|
repository.getMangaSuggestion(q, MAX_MANGA_ITEMS, src.takeIf { srcOnly })
|
||||||
.mapTo(result) {
|
.mapTo(result) {
|
||||||
@@ -89,4 +96,12 @@ class SearchSuggestionViewModel(
|
|||||||
suggestion.postValue(it)
|
suggestion.postValue(it)
|
||||||
}.launchIn(viewModelScope + Dispatchers.Default)
|
}.launchIn(viewModelScope + Dispatchers.Default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mapTags(tags: List<MangaTag>): List<ChipsView.ChipModel> = tags.map { tag ->
|
||||||
|
ChipsView.ChipModel(
|
||||||
|
icon = 0,
|
||||||
|
title = tag.title,
|
||||||
|
data = tag,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@ class SearchSuggestionAdapter(
|
|||||||
delegatesManager.addDelegate(ITEM_TYPE_MANGA, searchSuggestionMangaAD(coil, lifecycleOwner, listener))
|
delegatesManager.addDelegate(ITEM_TYPE_MANGA, searchSuggestionMangaAD(coil, lifecycleOwner, listener))
|
||||||
.addDelegate(ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
|
.addDelegate(ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
|
||||||
.addDelegate(ITEM_TYPE_HEADER, searchSuggestionHeaderAD(listener))
|
.addDelegate(ITEM_TYPE_HEADER, searchSuggestionHeaderAD(listener))
|
||||||
|
.addDelegate(ITEM_TYPE_TAGS, searchSuggestionTagsAD(listener))
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<SearchSuggestionItem>() {
|
private class DiffCallback : DiffUtil.ItemCallback<SearchSuggestionItem>() {
|
||||||
@@ -33,6 +34,7 @@ class SearchSuggestionAdapter(
|
|||||||
oldItem.query == newItem.query
|
oldItem.query == newItem.query
|
||||||
}
|
}
|
||||||
oldItem is SearchSuggestionItem.Header && newItem is SearchSuggestionItem.Header -> true
|
oldItem is SearchSuggestionItem.Header && newItem is SearchSuggestionItem.Header -> true
|
||||||
|
oldItem is SearchSuggestionItem.Tags && newItem is SearchSuggestionItem.Tags -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,5 +49,6 @@ class SearchSuggestionAdapter(
|
|||||||
const val ITEM_TYPE_MANGA = 0
|
const val ITEM_TYPE_MANGA = 0
|
||||||
const val ITEM_TYPE_QUERY = 1
|
const val ITEM_TYPE_QUERY = 1
|
||||||
const val ITEM_TYPE_HEADER = 2
|
const val ITEM_TYPE_HEADER = 2
|
||||||
|
const val ITEM_TYPE_TAGS = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||||
|
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
|
||||||
|
fun searchSuggestionTagsAD(
|
||||||
|
listener: SearchSuggestionListener,
|
||||||
|
) = adapterDelegate<SearchSuggestionItem.Tags, SearchSuggestionItem>(R.layout.item_search_suggestion_tags) {
|
||||||
|
|
||||||
|
val chipGroup = itemView as ChipsView
|
||||||
|
|
||||||
|
chipGroup.onChipClickListener = ChipsView.OnChipClickListener { _, data ->
|
||||||
|
listener.onTagClick(data as? MangaTag ?: return@OnChipClickListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
chipGroup.setChips(item.tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,26 @@
|
|||||||
package org.koitharu.kotatsu.search.ui.suggestion.model
|
package org.koitharu.kotatsu.search.ui.suggestion.model
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
|
||||||
sealed class SearchSuggestionItem {
|
sealed interface SearchSuggestionItem {
|
||||||
|
|
||||||
data class MangaItem(
|
data class MangaItem(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
) : SearchSuggestionItem()
|
) : SearchSuggestionItem
|
||||||
|
|
||||||
data class RecentQuery(
|
data class RecentQuery(
|
||||||
val query: String,
|
val query: String,
|
||||||
) : SearchSuggestionItem()
|
) : SearchSuggestionItem
|
||||||
|
|
||||||
data class Header(
|
data class Header(
|
||||||
val source: MangaSource,
|
val source: MangaSource,
|
||||||
val isChecked: MutableStateFlow<Boolean>,
|
val isChecked: MutableStateFlow<Boolean>,
|
||||||
) : SearchSuggestionItem()
|
) : SearchSuggestionItem
|
||||||
|
|
||||||
|
data class Tags(
|
||||||
|
val tags: List<ChipsView.ChipModel>,
|
||||||
|
) : SearchSuggestionItem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,10 +46,6 @@ class SuggestionsFragment : MangaListFragment() {
|
|||||||
|
|
||||||
override fun onScrolledToEnd() = Unit
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|
||||||
override fun getTitle(): CharSequence? {
|
|
||||||
return context?.getString(R.string.suggestions)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance() = SuggestionsFragment()
|
fun newInstance() = SuggestionsFragment()
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListen
|
|||||||
private var paddingVertical = 0
|
private var paddingVertical = 0
|
||||||
private var paddingHorizontal = 0
|
private var paddingHorizontal = 0
|
||||||
|
|
||||||
override fun getTitle() = context?.getString(R.string.updates)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
|||||||
11
app/src/main/res/layout/item_search_suggestion_tags.xml
Normal file
11
app/src/main/res/layout/item_search_suggestion_tags.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||||
|
android:paddingVertical="4dp"
|
||||||
|
app:chipSpacingHorizontal="6dp"
|
||||||
|
app:chipSpacingVertical="6dp" />
|
||||||
Reference in New Issue
Block a user