Add manga sources to search suggestion
This commit is contained in:
@@ -10,13 +10,6 @@ import androidx.collection.arraySetOf
|
|||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import java.io.File
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||||
@@ -25,6 +18,10 @@ import org.koitharu.kotatsu.utils.ext.getEnumValue
|
|||||||
import org.koitharu.kotatsu.utils.ext.observe
|
import org.koitharu.kotatsu.utils.ext.observe
|
||||||
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
||||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
||||||
|
import java.io.File
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class AppSettings(context: Context) {
|
class AppSettings(context: Context) {
|
||||||
|
|
||||||
@@ -195,10 +192,6 @@ class AppSettings(context: Context) {
|
|||||||
val isSuggestionsExcludeNsfw: Boolean
|
val isSuggestionsExcludeNsfw: Boolean
|
||||||
get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false)
|
get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false)
|
||||||
|
|
||||||
var isSearchSingleSource: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_SEARCH_SINGLE_SOURCE, false)
|
|
||||||
set(value) = prefs.edit { putBoolean(KEY_SEARCH_SINGLE_SOURCE, value) }
|
|
||||||
|
|
||||||
val dnsOverHttps: DoHProvider
|
val dnsOverHttps: DoHProvider
|
||||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
||||||
|
|
||||||
@@ -308,7 +301,6 @@ class AppSettings(context: Context) {
|
|||||||
const val KEY_SUGGESTIONS = "suggestions"
|
const val KEY_SUGGESTIONS = "suggestions"
|
||||||
const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw"
|
const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw"
|
||||||
const val KEY_SUGGESTIONS_EXCLUDE_TAGS = "suggestions_exclude_tags"
|
const val KEY_SUGGESTIONS_EXCLUDE_TAGS = "suggestions_exclude_tags"
|
||||||
const val KEY_SEARCH_SINGLE_SOURCE = "search_single_source"
|
|
||||||
const val KEY_SHIKIMORI = "shikimori"
|
const val KEY_SHIKIMORI = "shikimori"
|
||||||
const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism"
|
const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism"
|
||||||
const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown"
|
const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown"
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
|
import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -32,10 +31,10 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
|
|||||||
import org.koitharu.kotatsu.explore.ui.ExploreFragment
|
import org.koitharu.kotatsu.explore.ui.ExploreFragment
|
||||||
import org.koitharu.kotatsu.library.ui.LibraryFragment
|
import org.koitharu.kotatsu.library.ui.LibraryFragment
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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.model.MangaTag
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
|
||||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||||
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
|
||||||
@@ -187,12 +186,7 @@ class MainActivity :
|
|||||||
binding.searchView.query = query
|
binding.searchView.query = query
|
||||||
if (submit) {
|
if (submit) {
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty()) {
|
||||||
val source = searchSuggestionViewModel.getLocalSearchSource()
|
startActivity(MultiSearchActivity.newIntent(this, query))
|
||||||
if (source != null) {
|
|
||||||
startActivity(SearchActivity.newIntent(this, source, query))
|
|
||||||
} else {
|
|
||||||
startActivity(MultiSearchActivity.newIntent(this, query))
|
|
||||||
}
|
|
||||||
searchSuggestionViewModel.saveQuery(query)
|
searchSuggestionViewModel.saveQuery(query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,15 +213,13 @@ class MainActivity :
|
|||||||
voiceInputLauncher.tryLaunch(binding.searchView.hint?.toString(), options)
|
voiceInputLauncher.tryLaunch(binding.searchView.hint?.toString(), options)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClearSearchHistory() {
|
override fun onSourceToggle(source: MangaSource, isEnabled: Boolean) {
|
||||||
MaterialAlertDialogBuilder(this, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
|
searchSuggestionViewModel.onSourceToggle(source, isEnabled)
|
||||||
.setTitle(R.string.clear_search_history)
|
}
|
||||||
.setIcon(R.drawable.ic_clear_all)
|
|
||||||
.setMessage(R.string.text_clear_search_history_prompt)
|
override fun onSourceClick(source: MangaSource) {
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
val intent = MangaListActivity.newIntent(this, source)
|
||||||
.setPositiveButton(R.string.clear) { _, _ ->
|
startActivity(intent)
|
||||||
searchSuggestionViewModel.clearSearchHistory()
|
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||||
|
|||||||
@@ -91,6 +91,19 @@ class MangaSearchRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSourcesSuggestion(query: String, limit: Int): List<MangaSource> {
|
||||||
|
if (query.length < 3) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val sources = settings.remoteMangaSources
|
||||||
|
.filter { x -> x.title.contains(query, ignoreCase = true) }
|
||||||
|
return if (limit == 0) {
|
||||||
|
sources
|
||||||
|
} else {
|
||||||
|
sources.take(limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun saveSearchQuery(query: String) {
|
fun saveSearchQuery(query: String) {
|
||||||
recentSuggestions.saveRecentQuery(query, null)
|
recentSuggestions.saveRecentQuery(query, null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ 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.databinding.FragmentSearchSuggestionBinding
|
import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding
|
||||||
import org.koitharu.kotatsu.main.ui.AppBarOwner
|
|
||||||
import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter
|
import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter
|
||||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||||
|
|
||||||
class SearchSuggestionFragment :
|
class SearchSuggestionFragment :
|
||||||
BaseFragment<FragmentSearchSuggestionBinding>(),
|
BaseFragment<FragmentSearchSuggestionBinding>(),
|
||||||
@@ -34,7 +33,9 @@ class SearchSuggestionFragment :
|
|||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
listener = requireActivity() as SearchSuggestionListener,
|
listener = requireActivity() as SearchSuggestionListener,
|
||||||
)
|
)
|
||||||
|
addMenuProvider(SearchSuggestionMenuProvider(view.context, viewModel))
|
||||||
binding.root.adapter = adapter
|
binding.root.adapter = adapter
|
||||||
|
binding.root.setHasFixedSize(true)
|
||||||
viewModel.suggestion.observe(viewLifecycleOwner) {
|
viewModel.suggestion.observe(viewLifecycleOwner) {
|
||||||
adapter.items = it
|
adapter.items = it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.search.ui.suggestion
|
package org.koitharu.kotatsu.search.ui.suggestion
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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.model.MangaTag
|
||||||
|
|
||||||
interface SearchSuggestionListener {
|
interface SearchSuggestionListener {
|
||||||
@@ -11,7 +12,9 @@ interface SearchSuggestionListener {
|
|||||||
|
|
||||||
fun onQueryChanged(query: String)
|
fun onQueryChanged(query: String)
|
||||||
|
|
||||||
fun onClearSearchHistory()
|
fun onSourceToggle(source: MangaSource, isEnabled: Boolean)
|
||||||
|
|
||||||
|
fun onSourceClick(source: MangaSource)
|
||||||
|
|
||||||
fun onTagClick(tag: MangaTag)
|
fun onTagClick(tag: MangaTag)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
class SearchSuggestionMenuProvider(
|
||||||
|
private val context: Context,
|
||||||
|
private val viewModel: SearchSuggestionViewModel,
|
||||||
|
) : MenuProvider {
|
||||||
|
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.opt_search_suggestion, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
|
return when (menuItem.itemId) {
|
||||||
|
R.id.action_clear -> {
|
||||||
|
clearSearchHistory()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearSearchHistory() {
|
||||||
|
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
|
||||||
|
.setTitle(R.string.clear_search_history)
|
||||||
|
.setIcon(R.drawable.ic_clear_all)
|
||||||
|
.setMessage(R.string.text_clear_search_history_prompt)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.clear) { _, _ ->
|
||||||
|
viewModel.clearSearchHistory()
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.*
|
|||||||
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.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||||
@@ -16,6 +17,7 @@ private const val DEBOUNCE_TIMEOUT = 500L
|
|||||||
private const val MAX_MANGA_ITEMS = 6
|
private const val MAX_MANGA_ITEMS = 6
|
||||||
private const val MAX_QUERY_ITEMS = 16
|
private const val MAX_QUERY_ITEMS = 16
|
||||||
private const val MAX_TAGS_ITEMS = 8
|
private const val MAX_TAGS_ITEMS = 8
|
||||||
|
private const val MAX_SOURCES_ITEMS = 6
|
||||||
|
|
||||||
class SearchSuggestionViewModel(
|
class SearchSuggestionViewModel(
|
||||||
private val repository: MangaSearchRepository,
|
private val repository: MangaSearchRepository,
|
||||||
@@ -23,41 +25,36 @@ class SearchSuggestionViewModel(
|
|||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val query = MutableStateFlow("")
|
private val query = MutableStateFlow("")
|
||||||
private val source = MutableStateFlow<MangaSource?>(null)
|
|
||||||
private val isLocalSearch = MutableStateFlow(settings.isSearchSingleSource)
|
|
||||||
private var suggestionJob: Job? = null
|
private var suggestionJob: Job? = null
|
||||||
|
|
||||||
val suggestion = MutableLiveData<List<SearchSuggestionItem>>()
|
val suggestion = MutableLiveData<List<SearchSuggestionItem>>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setupSuggestion()
|
setupSuggestion()
|
||||||
isLocalSearch.onEach {
|
|
||||||
settings.isSearchSingleSource = it
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onQueryChanged(newQuery: String) {
|
fun onQueryChanged(newQuery: String) {
|
||||||
query.value = newQuery
|
query.value = newQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSourceChanged(newSource: MangaSource?) {
|
|
||||||
source.value = newSource
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveQuery(query: String) {
|
fun saveQuery(query: String) {
|
||||||
repository.saveSearchQuery(query)
|
repository.saveSearchQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalSearchSource(): MangaSource? {
|
|
||||||
return source.value?.takeIf { isLocalSearch.value }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearSearchHistory() {
|
fun clearSearchHistory() {
|
||||||
launchJob {
|
launchJob {
|
||||||
repository.clearSearchHistory()
|
repository.clearSearchHistory()
|
||||||
setupSuggestion()
|
setupSuggestion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onSourceToggle(source: MangaSource, isEnabled: Boolean) {
|
||||||
|
settings.hiddenSources = if (isEnabled) {
|
||||||
|
settings.hiddenSources - source.name
|
||||||
|
} else {
|
||||||
|
settings.hiddenSources + source.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteQuery(query: String) {
|
fun deleteQuery(query: String) {
|
||||||
launchJob {
|
launchJob {
|
||||||
@@ -70,11 +67,10 @@ class SearchSuggestionViewModel(
|
|||||||
suggestionJob?.cancel()
|
suggestionJob?.cancel()
|
||||||
suggestionJob = combine(
|
suggestionJob = combine(
|
||||||
query.debounce(DEBOUNCE_TIMEOUT),
|
query.debounce(DEBOUNCE_TIMEOUT),
|
||||||
source,
|
settings.observeAsFlow(AppSettings.KEY_SOURCES_HIDDEN) { hiddenSources },
|
||||||
isLocalSearch,
|
::Pair,
|
||||||
::Triple,
|
).mapLatest { (searchQuery, hiddenSources) ->
|
||||||
).mapLatest { (searchQuery, src, srcOnly) ->
|
buildSearchSuggestion(searchQuery, hiddenSources)
|
||||||
buildSearchSuggestion(searchQuery, src, srcOnly)
|
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
.onEach {
|
.onEach {
|
||||||
suggestion.postValue(it)
|
suggestion.postValue(it)
|
||||||
@@ -83,27 +79,24 @@ class SearchSuggestionViewModel(
|
|||||||
|
|
||||||
private suspend fun buildSearchSuggestion(
|
private suspend fun buildSearchSuggestion(
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
src: MangaSource?,
|
hiddenSources: Set<String>,
|
||||||
srcOnly: Boolean,
|
|
||||||
): List<SearchSuggestionItem> = coroutineScope {
|
): List<SearchSuggestionItem> = coroutineScope {
|
||||||
val queriesDeferred = async {
|
val queriesDeferred = async {
|
||||||
repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS)
|
repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS)
|
||||||
}
|
}
|
||||||
val tagsDeferred = async {
|
val tagsDeferred = async {
|
||||||
repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, src.takeIf { srcOnly })
|
repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null)
|
||||||
}
|
}
|
||||||
val mangaDeferred = async {
|
val mangaDeferred = async {
|
||||||
repository.getMangaSuggestion(searchQuery, MAX_MANGA_ITEMS, src.takeIf { srcOnly })
|
repository.getMangaSuggestion(searchQuery, MAX_MANGA_ITEMS, null)
|
||||||
}
|
}
|
||||||
|
val sources = repository.getSourcesSuggestion(searchQuery, MAX_SOURCES_ITEMS)
|
||||||
|
|
||||||
val tags = tagsDeferred.await()
|
val tags = tagsDeferred.await()
|
||||||
val mangaList = mangaDeferred.await()
|
val mangaList = mangaDeferred.await()
|
||||||
val queries = queriesDeferred.await()
|
val queries = queriesDeferred.await()
|
||||||
|
|
||||||
buildList(queries.size + 3) {
|
buildList(queries.size + sources.size + 2) {
|
||||||
if (src != null) {
|
|
||||||
add(SearchSuggestionItem.Header(src, isLocalSearch))
|
|
||||||
}
|
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
||||||
}
|
}
|
||||||
@@ -111,6 +104,7 @@ class SearchSuggestionViewModel(
|
|||||||
add(SearchSuggestionItem.MangaList(mangaList))
|
add(SearchSuggestionItem.MangaList(mangaList))
|
||||||
}
|
}
|
||||||
queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
||||||
|
sources.mapTo(this) { SearchSuggestionItem.Source(it, it.name !in hiddenSources) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class SearchSuggestionAdapter(
|
|||||||
init {
|
init {
|
||||||
delegatesManager
|
delegatesManager
|
||||||
.addDelegate(SEARCH_SUGGESTION_ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
|
.addDelegate(SEARCH_SUGGESTION_ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
|
||||||
.addDelegate(searchSuggestionHeaderAD(listener))
|
.addDelegate(searchSuggestionSourceAD(coil, lifecycleOwner, listener))
|
||||||
.addDelegate(searchSuggestionTagsAD(listener))
|
.addDelegate(searchSuggestionTagsAD(listener))
|
||||||
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
|
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,9 @@ class SearchSuggestionAdapter(
|
|||||||
oldItem is SearchSuggestionItem.RecentQuery && newItem is SearchSuggestionItem.RecentQuery -> {
|
oldItem is SearchSuggestionItem.RecentQuery && newItem is SearchSuggestionItem.RecentQuery -> {
|
||||||
oldItem.query == newItem.query
|
oldItem.query == newItem.query
|
||||||
}
|
}
|
||||||
|
oldItem is SearchSuggestionItem.Source && newItem is SearchSuggestionItem.Source -> {
|
||||||
|
oldItem.source == newItem.source
|
||||||
|
}
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,5 +43,15 @@ class SearchSuggestionAdapter(
|
|||||||
oldItem: SearchSuggestionItem,
|
oldItem: SearchSuggestionItem,
|
||||||
newItem: SearchSuggestionItem,
|
newItem: SearchSuggestionItem,
|
||||||
): Boolean = Intrinsics.areEqual(oldItem, newItem)
|
): Boolean = Intrinsics.areEqual(oldItem, newItem)
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItem: SearchSuggestionItem, newItem: SearchSuggestionItem): Any? {
|
||||||
|
return when {
|
||||||
|
oldItem is SearchSuggestionItem.MangaList && newItem is SearchSuggestionItem.MangaList -> Unit
|
||||||
|
oldItem is SearchSuggestionItem.Source && newItem is SearchSuggestionItem.Source -> {
|
||||||
|
if (oldItem.isEnabled != newItem.isEnabled) Unit else super.getChangePayload(oldItem, newItem)
|
||||||
|
}
|
||||||
|
else -> super.getChangePayload(oldItem, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
|
||||||
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionHeaderBinding
|
|
||||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
|
||||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
|
||||||
|
|
||||||
fun searchSuggestionHeaderAD(
|
|
||||||
listener: SearchSuggestionListener,
|
|
||||||
) = adapterDelegateViewBinding<SearchSuggestionItem.Header, SearchSuggestionItem, ItemSearchSuggestionHeaderBinding>(
|
|
||||||
{ inflater, parent -> ItemSearchSuggestionHeaderBinding.inflate(inflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
binding.switchLocal.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
item.isChecked.value = isChecked
|
|
||||||
}
|
|
||||||
binding.buttonClear.setOnClickListener {
|
|
||||||
listener.onClearSearchHistory()
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.switchLocal.text = getString(
|
|
||||||
R.string.search_only_on_s,
|
|
||||||
item.source.title,
|
|
||||||
)
|
|
||||||
binding.switchLocal.isChecked = item.isChecked.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.request.Disposable
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceBinding
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable
|
||||||
|
|
||||||
|
fun searchSuggestionSourceAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
listener: SearchSuggestionListener,
|
||||||
|
) = adapterDelegateViewBinding<SearchSuggestionItem.Source, SearchSuggestionItem, ItemSearchSuggestionSourceBinding>(
|
||||||
|
{ inflater, parent -> ItemSearchSuggestionSourceBinding.inflate(inflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
var imageRequest: Disposable? = null
|
||||||
|
|
||||||
|
binding.switchLocal.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
listener.onSourceToggle(item.source, isChecked)
|
||||||
|
}
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
listener.onSourceClick(item.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.text = item.source.title
|
||||||
|
binding.switchLocal.isChecked = item.isEnabled
|
||||||
|
val fallbackIcon = FaviconFallbackDrawable(context, item.source.name)
|
||||||
|
imageRequest = ImageRequest.Builder(context)
|
||||||
|
.data(item.faviconUrl)
|
||||||
|
.fallback(fallbackIcon)
|
||||||
|
.placeholder(fallbackIcon)
|
||||||
|
.error(fallbackIcon)
|
||||||
|
.target(binding.imageViewCover)
|
||||||
|
.lifecycle(lifecycleOwner)
|
||||||
|
.enqueueWith(coil)
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewRecycled {
|
||||||
|
imageRequest?.dispose()
|
||||||
|
imageRequest = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.search.ui.suggestion.model
|
package org.koitharu.kotatsu.search.ui.suggestion.model
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import android.net.Uri
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
@@ -52,26 +52,29 @@ sealed interface SearchSuggestionItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Header(
|
class Source(
|
||||||
val source: MangaSource,
|
val source: MangaSource,
|
||||||
val isChecked: MutableStateFlow<Boolean>,
|
val isEnabled: Boolean,
|
||||||
) : SearchSuggestionItem {
|
) : SearchSuggestionItem {
|
||||||
|
|
||||||
|
val faviconUrl: Uri
|
||||||
|
get() = Uri.fromParts("favicon", source.name, null)
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
other as Header
|
other as Source
|
||||||
|
|
||||||
if (source != other.source) return false
|
if (source != other.source) return false
|
||||||
if (isChecked !== other.isChecked) return false
|
if (isEnabled != other.isEnabled) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = source.hashCode()
|
var result = source.hashCode()
|
||||||
result = 31 * result + isChecked.hashCode()
|
result = 31 * result + isEnabled.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:minHeight="?listPreferredItemHeightSmall"
|
|
||||||
android:paddingStart="?listPreferredItemPaddingStart"
|
|
||||||
android:paddingEnd="?listPreferredItemPaddingEnd">
|
|
||||||
|
|
||||||
<com.google.android.material.materialswitch.MaterialSwitch
|
|
||||||
android:id="@+id/switch_local"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="?listPreferredItemPaddingEnd"
|
|
||||||
android:layout_weight="1"
|
|
||||||
tools:text="@string/search_only_on_s" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/button_clear"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:src="@drawable/ic_clear_all" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
45
app/src/main/res/layout/item_search_suggestion_source.xml
Normal file
45
app/src/main/res/layout/item_search_suggestion_source.xml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/imageView_cover"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="?listPreferredItemPaddingStart"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:shapeAppearance="?shapeAppearanceCornerSmall"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="@dimen/margin_small"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
tools:text="@tools:sample/lorem[2]" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="1dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginVertical="8dp"
|
||||||
|
android:background="?colorOutline" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switch_local"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingHorizontal="?listPreferredItemPaddingEnd" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
11
app/src/main/res/menu/opt_search_suggestion.xml
Normal file
11
app/src/main/res/menu/opt_search_suggestion.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_clear"
|
||||||
|
android:title="@string/clear_search_history"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
Reference in New Issue
Block a user