Add manga sources to search suggestion

This commit is contained in:
Koitharu
2022-07-08 13:55:37 +03:00
parent dd8cb8dfd0
commit 7d41318d15
14 changed files with 222 additions and 122 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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