Search manga with filters

This commit is contained in:
Koitharu
2024-09-22 17:42:28 +03:00
parent 98314960cf
commit 66644d55a4
33 changed files with 309 additions and 441 deletions

View File

@@ -83,7 +83,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:f2354957e6') {
implementation('com.github.KotatsuApp:kotatsu-parsers:f410df40f1') {
exclude group: 'org.json', module: 'json'
}

View File

@@ -68,7 +68,7 @@
</intent-filter>
<meta-data
android:name="android.app.default_searchable"
android:value="org.koitharu.kotatsu.ui.search.SearchActivity" />
android:value="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity" />
</activity>
<activity
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity"
@@ -112,9 +112,6 @@
android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/remote_action" />
</activity>
<activity
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
android:label="@string/search" />
<activity
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
android:exported="true"

View File

@@ -30,7 +30,8 @@ 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.parsers.model.Manga
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.search.ui.MangaListActivity
import javax.inject.Inject
@AndroidEntryPoint
@@ -81,7 +82,14 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
override fun onItemClick(item: MangaAlternativeModel, view: View) {
when (view.id) {
R.id.chip_source -> startActivity(SearchActivity.newIntent(this, item.manga.source, viewModel.manga.title))
R.id.chip_source -> startActivity(
MangaListActivity.newIntent(
this,
item.manga.source,
MangaListFilter(query = viewModel.manga.title),
),
)
R.id.button_migrate -> confirmMigration(item.manga)
else -> startActivity(DetailsActivity.newIntent(this, item.manga))
}

View File

@@ -1,10 +1,13 @@
package org.koitharu.kotatsu.core.model
import android.net.Uri
import android.text.SpannableStringBuilder
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.collection.MutableObjectIntMap
import androidx.core.os.LocaleListCompat
import androidx.core.text.buildSpannedString
import androidx.core.text.strikeThrough
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.iterator
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
@@ -12,6 +15,7 @@ import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Demographic
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.util.formatSimple
import org.koitharu.kotatsu.parsers.util.mapToSet
@@ -152,3 +156,26 @@ fun Manga.chaptersCount(): Int {
}
return max
}
fun MangaListFilter.getSummary() = buildSpannedString {
if (!query.isNullOrEmpty()) {
append(query)
if (tags.isNotEmpty() || tagsExclude.isNotEmpty()) {
append(' ')
append('(')
appendTagsSummary(this@getSummary)
append(')')
}
} else {
appendTagsSummary(this@getSummary)
}
}
private fun SpannableStringBuilder.appendTagsSummary(filter: MangaListFilter) {
filter.tags.joinTo(this) { it.title }
if (filter.tagsExclude.isNotEmpty()) {
strikeThrough {
filter.tagsExclude.joinTo(this) { it.title }
}
}
}

View File

@@ -0,0 +1,53 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler
import org.koitharu.kotatsu.core.util.ext.readEnumSet
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
import org.koitharu.kotatsu.core.util.ext.writeEnumSet
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Demographic
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaState
object MangaListFilterParceler : Parceler<MangaListFilter> {
override fun MangaListFilter.write(parcel: Parcel, flags: Int) {
parcel.writeString(query)
parcel.writeParcelable(ParcelableMangaTags(tags), 0)
parcel.writeParcelable(ParcelableMangaTags(tagsExclude), 0)
parcel.writeSerializable(locale)
parcel.writeSerializable(originalLocale)
parcel.writeEnumSet(states)
parcel.writeEnumSet(contentRating)
parcel.writeEnumSet(types)
parcel.writeEnumSet(demographics)
parcel.writeInt(year)
parcel.writeInt(yearFrom)
parcel.writeInt(yearTo)
}
override fun create(parcel: Parcel) = MangaListFilter(
query = parcel.readString(),
tags = parcel.readParcelableCompat<ParcelableMangaTags>()?.tags.orEmpty(),
tagsExclude = parcel.readParcelableCompat<ParcelableMangaTags>()?.tags.orEmpty(),
locale = parcel.readSerializableCompat(),
originalLocale = parcel.readSerializableCompat(),
states = parcel.readEnumSet<MangaState>().orEmpty(),
contentRating = parcel.readEnumSet<ContentRating>().orEmpty(),
types = parcel.readEnumSet<ContentType>().orEmpty(),
demographics = parcel.readEnumSet<Demographic>().orEmpty(),
year = parcel.readInt(),
yearFrom = parcel.readInt(),
yearTo = parcel.readInt(),
)
}
@Parcelize
@TypeParceler<MangaListFilter, MangaListFilterParceler>
data class ParcelableMangaListFilter(val filter: MangaListFilter) : Parcelable

View File

@@ -180,7 +180,7 @@ class AppShortcutManager @Inject constructor(
.setLongLabel(title)
.setIcon(icon)
.setLongLived(true)
.setIntent(MangaListActivity.newIntent(context, source))
.setIntent(MangaListActivity.newIntent(context, source, null))
.build()
}
}

View File

@@ -36,11 +36,6 @@ class ChipsView @JvmOverloads constructor(
children.forEach { it.isClickable = isChipClickable }
}
var onChipCloseClickListener: OnChipCloseClickListener? = null
set(value) {
field = value
val isCloseIconVisible = value != null
children.forEach { (it as? Chip)?.isCloseIconVisible = isCloseIconVisible }
}
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0)
@@ -98,6 +93,7 @@ class ChipsView @JvmOverloads constructor(
@ColorRes val tint: Int = 0,
val isChecked: Boolean = false,
val isDropdown: Boolean = false,
val isCloseable: Boolean = false,
val data: Any? = null,
)
@@ -139,7 +135,7 @@ class ChipsView @JvmOverloads constructor(
isChipIconVisible = true
}
isCheckedIconVisible = model.isChecked
isCloseIconVisible = if (onChipCloseClickListener != null || model.isDropdown) {
isCloseIconVisible = if (model.isCloseable || model.isDropdown) {
setCloseIconResource(
if (model.isDropdown) R.drawable.ic_expand_more else materialR.drawable.ic_m3_chip_close,
)

View File

@@ -12,6 +12,7 @@ import androidx.core.os.BundleCompat
import androidx.core.os.ParcelCompat
import androidx.lifecycle.SavedStateHandle
import java.io.Serializable
import java.util.EnumSet
// https://issuetracker.google.com/issues/240585930
@@ -53,6 +54,31 @@ inline fun <reified T : Serializable> Bundle.requireSerializable(key: String): T
}
}
fun <E : Enum<E>> Parcel.writeEnumSet(set: Set<E>?) {
if (set == null) {
writeValue(null)
} else {
val array = IntArray(set.size)
set.forEachIndexed { i, e -> array[i] = e.ordinal }
writeIntArray(array)
}
}
inline fun <reified E : Enum<E>> Parcel.readEnumSet(): Set<E>? = readEnumSet(E::class.java)
fun <E : Enum<E>> Parcel.readEnumSet(cls: Class<E>): Set<E>? {
val array = createIntArray() ?: return null
if (array.isEmpty()) {
return emptySet()
}
val enumValues = cls.enumConstants ?: return null
val set = EnumSet.noneOf(cls)
array.forEach { e ->
set.add(enumValues[e])
}
return set
}
fun <T> SavedStateHandle.require(key: String): T {
return checkNotNull(get(key)) {
"Value $key not found in SavedStateHandle or has a wrong type"

View File

@@ -25,6 +25,12 @@ fun <T> Collection<T>.asArrayList(): ArrayList<T> = if (this is ArrayList<*>) {
ArrayList(this)
}
fun <E : Enum<E>> Set<E>.asEnumSet(cls: Class<E>): EnumSet<E> = if (this is EnumSet<*>) {
this as EnumSet<E>
} else {
EnumSet.noneOf(cls).apply { addAll(this@asEnumSet) }
}
fun <K, V> Map<K, V>.findKeyByValue(value: V): K? {
for ((k, v) in entries) {
if (v == value) {

View File

@@ -98,13 +98,13 @@ import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
import javax.inject.Inject
import com.google.android.material.R as materialR
@@ -213,10 +213,10 @@ class DetailsActivity :
R.id.chip_author -> {
val manga = viewModel.manga.value ?: return
startActivity(
SearchActivity.newIntent(
MangaListActivity.newIntent(
context = v.context,
source = manga.source,
query = manga.author ?: return,
filter = MangaListFilter(query = manga.author),
),
)
}
@@ -227,6 +227,7 @@ class DetailsActivity :
MangaListActivity.newIntent(
context = v.context,
source = manga.source,
filter = null,
),
)
}
@@ -286,7 +287,8 @@ class DetailsActivity :
override fun onChipClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag ?: return
startActivity(MangaListActivity.newIntent(this, setOf(tag)))
// TODO dialog
startActivity(MangaListActivity.newIntent(this, tag.source, MangaListFilter(tags = setOf(tag))))
}
override fun onLongClick(v: View): Boolean = when (v.id) {

View File

@@ -265,7 +265,7 @@ class DownloadNotificationFactory @AssistedInject constructor(
if (manga != null) {
DetailsActivity.newIntent(context, manga)
} else {
MangaListActivity.newIntent(context, LocalMangaSource)
MangaListActivity.newIntent(context, LocalMangaSource, null)
},
PendingIntent.FLAG_CANCEL_CURRENT,
false,

View File

@@ -126,7 +126,7 @@ class ExploreFragment :
override fun onClick(v: View) {
val intent = when (v.id) {
R.id.button_local -> MangaListActivity.newIntent(v.context, LocalMangaSource)
R.id.button_local -> MangaListActivity.newIntent(v.context, LocalMangaSource, null)
R.id.button_bookmarks -> AllBookmarksActivity.newIntent(v.context)
R.id.button_more -> SuggestionsActivity.newIntent(v.context)
R.id.button_downloads -> Intent(v.context, DownloadsActivity::class.java)
@@ -144,7 +144,7 @@ class ExploreFragment :
if (sourceSelectionController?.onItemClick(item.id) == true) {
return
}
val intent = MangaListActivity.newIntent(view.context, item.source)
val intent = MangaListActivity.newIntent(view.context, item.source, null)
startActivity(intent)
}

View File

@@ -59,8 +59,8 @@ class FilterCoordinator @Inject constructor(
private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder)
private val availableSortOrders = repository.sortOrders
private val capabilities = repository.filterCapabilities
private val filterOptions = SuspendLazy { repository.getFilterOptions() }
val capabilities = repository.filterCapabilities
val mangaSource: MangaSource
get() = repository.source
@@ -69,7 +69,7 @@ class FilterCoordinator @Inject constructor(
get() = !currentListFilter.value.isEmpty()
val query: StateFlow<String?> = currentListFilter.map { it.query }
.stateIn(coroutineScope, SharingStarted.Lazily, null)
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
val sortOrder: StateFlow<FilterProperty<SortOrder>> = currentSortOrder.map { selected ->
FilterProperty(

View File

@@ -22,7 +22,8 @@ import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener {
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener,
ChipsView.OnChipCloseClickListener {
@Inject
lateinit var filterHeaderProducer: FilterHeaderProducer
@@ -37,6 +38,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
binding.chipsTags.onChipClickListener = this
binding.chipsTags.onChipCloseClickListener = this
filterHeaderProducer.observeHeader(filter)
.flowOn(Dispatchers.Default)
.observe(viewLifecycleOwner, ::onDataChanged)
@@ -45,11 +47,16 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
override fun onWindowInsetsChanged(insets: Insets) = Unit
override fun onChipClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag
if (tag == null) {
TagsCatalogSheet.show(parentFragmentManager, isExcludeTag = false)
} else {
filter.toggleTag(tag, !chip.isChecked)
when (data) {
is MangaTag -> filter.toggleTag(data, !chip.isChecked)
is String -> Unit
null -> TagsCatalogSheet.show(parentFragmentManager, isExcludeTag = false)
}
}
override fun onChipCloseClick(chip: Chip, data: Any?) {
when (data) {
is String -> filter.setQuery(null)
}
}

View File

@@ -2,25 +2,25 @@ package org.koitharu.kotatsu.filter.ui
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.mapLatest
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import java.util.LinkedList
import javax.inject.Inject
import com.google.android.material.R as materialR
class FilterHeaderProducer @Inject constructor(
private val searchRepository: MangaSearchRepository,
) {
fun observeHeader(filterCoordinator: FilterCoordinator): Flow<FilterHeaderModel> {
return filterCoordinator.tags.mapLatest {
return combine(filterCoordinator.tags, filterCoordinator.query) { tags, query ->
createChipsList(
source = filterCoordinator.mangaSource,
property = it,
property = tags,
query = query,
limit = 8,
)
}.combine(filterCoordinator.observe()) { chipList, snapshot ->
@@ -35,6 +35,7 @@ class FilterHeaderProducer @Inject constructor(
private suspend fun createChipsList(
source: MangaSource,
property: FilterProperty<MangaTag>,
query: String?,
limit: Int,
): List<ChipsView.ChipModel> {
val selectedTags = property.selectedItems.toMutableSet()
@@ -49,7 +50,7 @@ class FilterHeaderProducer @Inject constructor(
if (tags.isEmpty() && selectedTags.isEmpty()) {
return emptyList()
}
val result = LinkedList<ChipsView.ChipModel>()
val result = ArrayDeque<ChipsView.ChipModel>(tags.size + selectedTags.size + 1)
for (tag in tags) {
val model = ChipsView.ChipModel(
title = tag.title,
@@ -70,6 +71,16 @@ class FilterHeaderProducer @Inject constructor(
)
result.addFirst(model)
}
if (!query.isNullOrEmpty()) {
result.addFirst(
ChipsView.ChipModel(
title = query,
icon = materialR.drawable.abc_ic_search_api_material,
isCloseable = true,
data = query,
),
)
}
return result
}
}

View File

@@ -60,6 +60,7 @@ import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
import org.koitharu.kotatsu.main.ui.MainActivity
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.Manga
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
@@ -164,7 +165,8 @@ abstract class MangaListFragment :
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) {
if (selectionController?.onItemClick(manga.id) != true) {
val intent = MangaListActivity.newIntent(context ?: return, setOf(tag))
// TODO dialog
val intent = MangaListActivity.newIntent(view.context, tag.source, MangaListFilter(tags = setOf(tag)))
startActivity(intent)
}
}

View File

@@ -31,10 +31,10 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity
import javax.inject.Inject
@AndroidEntryPoint
@@ -83,10 +83,10 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
}
R.id.textView_author -> startActivity(
SearchActivity.newIntent(
MangaListActivity.newIntent(
context = v.context,
source = manga.source,
query = manga.author ?: return,
filter = MangaListFilter(query = manga.author),
),
)
@@ -107,7 +107,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
val tag = data as? MangaTag ?: return
val filter = (activity as? FilterCoordinator.Owner)?.filterCoordinator
if (filter == null) {
startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag)))
startActivity(MangaListActivity.newIntent(chip.context, tag.source, MangaListFilter(tags = setOf(tag))))
} else {
filter.toggleTag(tag, true)
closeSelf()

View File

@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.remotelist.ui.MangaSearchMenuProvider
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
@@ -61,6 +62,7 @@ class LocalListFragment : MangaListFragment(), FilterCoordinator.Owner {
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
addMenuProvider(LocalListMenuProvider(binding.root.context, this::onEmptyActionClick))
addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel))
viewModel.onMangaRemoved.observeEvent(viewLifecycleOwner) { onItemRemoved() }
}

View File

@@ -16,7 +16,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.filter.ui.FilterHeaderProducer
import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -41,7 +40,6 @@ class LocalListViewModel @Inject constructor(
exploreRepository: ExploreRepository,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
private val localStorageManager: LocalStorageManager,
filterHeaderProducer: FilterHeaderProducer,
sourcesRepository: MangaSourcesRepository,
) : RemoteListViewModel(
savedStateHandle,
@@ -109,8 +107,10 @@ class LocalListViewModel @Inject constructor(
}
}
override fun createEmptyState(canResetFilter: Boolean): EmptyState {
return EmptyState(
override fun createEmptyState(canResetFilter: Boolean): EmptyState = if (canResetFilter) {
super.createEmptyState(canResetFilter)
} else {
EmptyState(
icon = R.drawable.ic_empty_local,
textPrimary = R.string.text_local_holder_primary,
textSecondary = R.string.text_local_holder_secondary,

View File

@@ -60,6 +60,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
@@ -265,7 +266,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
}
override fun onTagClick(tag: MangaTag) {
startActivity(MangaListActivity.newIntent(this, setOf(tag)))
startActivity(MangaListActivity.newIntent(this, tag.source, MangaListFilter(tags = setOf(tag))))
}
override fun onQueryChanged(query: String) {
@@ -277,7 +278,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
}
override fun onSourceClick(source: MangaSource) {
val intent = MangaListActivity.newIntent(this, source)
val intent = MangaListActivity.newIntent(this, source, null)
startActivity(intent)
}

View File

@@ -0,0 +1,69 @@
package org.koitharu.kotatsu.remotelist.ui
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import androidx.core.view.inputmethod.EditorInfoCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.parsers.model.MangaListFilter
class MangaSearchMenuProvider(
private val filter: FilterCoordinator,
private val viewModel: MangaListViewModel,
) : MenuProvider, MenuItem.OnActionExpandListener, SearchView.OnQueryTextListener {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_search, menu)
val menuItem = menu.findItem(R.id.action_search)
menuItem.setOnActionExpandListener(this)
val searchView = menuItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.queryHint = menuItem.title
}
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_search)?.isVisible = filter.capabilities.isSearchSupported
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false
override fun onQueryTextSubmit(query: String?): Boolean {
val snapshot = filter.snapshot()
if (!query.isNullOrEmpty() && !filter.capabilities.isSearchWithFiltersSupported && snapshot.listFilter.hasNonSearchOptions()) {
filter.set(MangaListFilter(query = query))
viewModel.onActionDone.call(
ReversibleAction(R.string.filter_search_warning) { filter.set(snapshot.listFilter) },
)
} else {
filter.setQuery(query)
}
return true
}
override fun onQueryTextChange(newText: String?): Boolean = false
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
(item.actionView as? SearchView)?.run {
post { adjustSearchView() }
}
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean = true
private fun SearchView.adjustSearchView() {
imeOptions = if (viewModel.isIncognitoModeEnabled) {
imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
} else {
imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
}
setQuery(filter.query.value, false)
}
}

View File

@@ -6,9 +6,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.fragment.app.viewModels
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@@ -28,9 +26,7 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.settings.SettingsActivity
@AndroidEntryPoint
@@ -44,6 +40,7 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
addMenuProvider(RemoteListMenuProvider())
addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel))
viewModel.isRandomLoading.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
viewModel.onOpenManga.observeEvent(viewLifecycleOwner) {
startActivity(DetailsActivity.newIntent(binding.root.context, it))
@@ -86,19 +83,10 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
.show()
}
private inner class RemoteListMenuProvider :
MenuProvider,
SearchView.OnQueryTextListener,
MenuItem.OnActionExpandListener {
private inner class RemoteListMenuProvider : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_list_remote, menu)
val searchMenuItem = menu.findItem(R.id.action_search)
searchMenuItem.setOnActionExpandListener(this)
val searchView = searchMenuItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.setIconifiedByDefault(false)
searchView.queryHint = searchMenuItem.title
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
@@ -127,43 +115,9 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_search)?.isVisible = viewModel.isSearchAvailable
menu.findItem(R.id.action_random)?.isEnabled = !viewModel.isRandomLoading.value
menu.findItem(R.id.action_filter_reset)?.isVisible = filterCoordinator.isFilterApplied
}
override fun onQueryTextSubmit(query: String?): Boolean {
if (query.isNullOrEmpty()) {
return false
}
val intent = SearchActivity.newIntent(
context = this@RemoteListFragment.context ?: return false,
source = viewModel.source,
query = query,
)
startActivity(intent)
return true
}
override fun onQueryTextChange(newText: String?): Boolean = false
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
(item.actionView as? SearchView)?.run {
imeOptions = if (viewModel.isIncognitoModeEnabled) {
imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
} else {
imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
}
}
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
val searchView = (item.actionView as? SearchView) ?: return false
searchView.setQuery("", false)
return true
}
}
companion object {

View File

@@ -70,9 +70,6 @@ open class RemoteListViewModel @Inject constructor(
private var loadingJob: Job? = null
private var randomJob: Job? = null
val isSearchAvailable: Boolean
get() = repository.filterCapabilities.isSearchSupported
val browserUrl: String?
get() = (repository as? ParserMangaRepository)?.domain?.let { "https://$it" }

View File

@@ -23,10 +23,11 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaListFilter
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.model.titleRes
@@ -45,7 +46,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.isNullOrEmpty
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import kotlin.math.absoluteValue
import com.google.android.material.R as materialR
@@ -63,28 +64,23 @@ class MangaListActivity :
"Cannot find FilterCoordinator.Owner fragment in ${supportFragmentManager.fragments}"
}.filterCoordinator
private var source: MangaSource? = null
private lateinit var source: MangaSource
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityMangaListBinding.inflate(layoutInflater))
val tags = intent.getParcelableExtraCompat<ParcelableMangaTags>(EXTRA_TAGS)?.tags
val filter = intent.getParcelableExtraCompat<ParcelableMangaListFilter>(EXTRA_FILTER)?.filter
source = MangaSource(intent.getStringExtra(EXTRA_SOURCE))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (viewBinding.containerFilterHeader != null) {
viewBinding.appbar.addOnOffsetChangedListener(this)
}
source = intent.getStringExtra(EXTRA_SOURCE)?.let(::MangaSource) ?: tags?.firstOrNull()?.source
val src = source
if (src == null) {
finishAfterTransition()
} else {
viewBinding.buttonOrder?.setOnClickListener(this)
title = src.getTitle(this)
initList(src, tags)
}
viewBinding.buttonOrder?.setOnClickListener(this)
title = source.getTitle(this)
initList(source, filter)
}
override fun isNsfwContent(): Flow<Boolean> = flowOf(source?.isNsfw() == true)
override fun isNsfwContent(): Flow<Boolean> = flowOf(source.isNsfw())
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding(
@@ -119,7 +115,7 @@ class MangaListActivity :
fun hidePreview() = setSideFragment(FilterSheetFragment::class.java, null)
private fun initList(source: MangaSource, tags: Set<MangaTag>?) {
private fun initList(source: MangaSource, filter: MangaListFilter?) {
val fm = supportFragmentManager
val existingFragment = fm.findFragmentById(R.id.container)
if (existingFragment is FilterCoordinator.Owner) {
@@ -134,8 +130,8 @@ class MangaListActivity :
}
replace(R.id.container, fragment)
runOnCommit { initFilter(fragment) }
if (!tags.isNullOrEmpty()) {
runOnCommit(ApplyFilterRunnable(fragment, tags))
if (filter != null) {
runOnCommit(ApplyFilterRunnable(fragment, filter))
}
}
}
@@ -161,11 +157,12 @@ class MangaListActivity :
filterBadge.setMaxCharacterCount(0)
filter.observe().observe(this) { snapshot ->
chipSort.setTextAndVisible(snapshot.sortOrder.titleRes)
filterBadge.counter = if (snapshot.listFilter.isEmpty()) 0 else 1
filterBadge.counter = if (snapshot.listFilter.hasNonSearchOptions()) 1 else 0
supportActionBar?.subtitle = snapshot.listFilter.query
}
} else {
filter.observe().map {
it.listFilter.tags.joinToString { tag -> tag.title }
it.listFilter.getSummary()
}.flowOn(Dispatchers.Default)
.observe(this) {
supportActionBar?.subtitle = it
@@ -189,26 +186,28 @@ class MangaListActivity :
private class ApplyFilterRunnable(
private val filterOwner: FilterCoordinator.Owner,
private val tags: Set<MangaTag>,
private val filter: MangaListFilter,
) : Runnable {
override fun run() {
filterOwner.filterCoordinator.set(MangaListFilter(tags = tags))
filterOwner.filterCoordinator.set(filter)
}
}
companion object {
private const val EXTRA_TAGS = "tags"
private const val EXTRA_FILTER = "filter"
private const val EXTRA_SOURCE = "source"
const val ACTION_MANGA_EXPLORE = "${BuildConfig.APPLICATION_ID}.action.EXPLORE_MANGA"
private const val ACTION_MANGA_EXPLORE = "${BuildConfig.APPLICATION_ID}.action.EXPLORE_MANGA"
fun newIntent(context: Context, tags: Set<MangaTag>) = Intent(context, MangaListActivity::class.java)
.setAction(ACTION_MANGA_EXPLORE)
.putExtra(EXTRA_TAGS, ParcelableMangaTags(tags))
fun newIntent(context: Context, source: MangaSource) = Intent(context, MangaListActivity::class.java)
.setAction(ACTION_MANGA_EXPLORE)
.putExtra(EXTRA_SOURCE, source.name)
fun newIntent(context: Context, source: MangaSource, filter: MangaListFilter?): Intent =
Intent(context, MangaListActivity::class.java)
.setAction(ACTION_MANGA_EXPLORE)
.putExtra(EXTRA_SOURCE, source.name)
.apply {
if (!filter.isNullOrEmpty()) {
putExtra(EXTRA_FILTER, ParcelableMangaListFilter(filter))
}
}
}
}

View File

@@ -1,97 +0,0 @@
package org.koitharu.kotatsu.search.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
import androidx.core.view.SoftwareKeyboardControllerCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.ActivitySearchBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
@AndroidEntryPoint
class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQueryTextListener {
private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>()
private lateinit var source: MangaSource
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySearchBinding.inflate(layoutInflater))
source = MangaSource(intent.getStringExtra(EXTRA_SOURCE))
val query = intent.getStringExtra(EXTRA_QUERY)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
with(viewBinding.searchView) {
queryHint = getString(R.string.search_on_s, source.getTitle(context))
setOnQueryTextListener(this@SearchActivity)
if (query.isNullOrBlank()) {
requestFocus()
SoftwareKeyboardControllerCompat(this).show()
} else {
setQuery(query, true)
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.toolbar.updatePadding(
left = insets.left,
right = insets.right,
top = insets.top,
)
viewBinding.container.updatePadding(
bottom = insets.bottom,
)
}
override fun onQueryTextSubmit(query: String?): Boolean {
val q = query?.trim()
if (q.isNullOrEmpty()) {
return false
}
title = query
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.container, SearchFragment.newInstance(source, q))
}
viewBinding.searchView.clearFocus()
searchSuggestionViewModel.saveQuery(q)
return true
}
override fun onQueryTextChange(newText: String?): Boolean = false
private fun onIncognitoModeChanged(isIncognito: Boolean) {
var options = viewBinding.searchView.imeOptions
options = if (isIncognito) {
options or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
} else {
options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
}
viewBinding.searchView.imeOptions = options
}
companion object {
private const val EXTRA_SOURCE = "source"
private const val EXTRA_QUERY = "query"
fun newIntent(context: Context, source: MangaSource, query: String?) =
Intent(context, SearchActivity::class.java)
.putExtra(EXTRA_SOURCE, source.name)
.putExtra(EXTRA_QUERY, query)
}
}

View File

@@ -1,37 +0,0 @@
package org.koitharu.kotatsu.search.ui
import android.view.Menu
import androidx.appcompat.view.ActionMode
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource
@AndroidEntryPoint
class SearchFragment : MangaListFragment() {
override val viewModel by viewModels<SearchViewModel>()
override fun onScrolledToEnd() {
viewModel.loadNextPage()
}
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_remote, menu)
return super.onCreateActionMode(controller, mode, menu)
}
companion object {
const val ARG_QUERY = "query"
const val ARG_SOURCE = "source"
fun newInstance(source: MangaSource, query: String) = SearchFragment().withArgs(2) {
putString(ARG_SOURCE, source.name)
putString(ARG_QUERY, query)
}
}
}

View File

@@ -1,129 +0,0 @@
package org.koitharu.kotatsu.search.ui
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.distinctById
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.MangaListViewModel
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.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import javax.inject.Inject
@HiltViewModel
class SearchViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
repositoryFactory: MangaRepository.Factory,
settings: AppSettings,
private val mangaListMapper: MangaListMapper,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
private val query = savedStateHandle.require<String>(SearchFragment.ARG_QUERY)
private val repository = repositoryFactory.create(MangaSource(savedStateHandle[SearchFragment.ARG_SOURCE]))
private val mangaList = MutableStateFlow<List<Manga>?>(null)
private val hasNextPage = MutableStateFlow(false)
private val listError = MutableStateFlow<Throwable?>(null)
private var loadingJob: Job? = null
override val content = combine(
mangaList.map { it?.skipNsfwIfNeeded() },
observeListModeWithTriggers(),
listError,
hasNextPage,
) { list, mode, error, hasNext ->
when {
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState)
list.isEmpty() -> listOf(
EmptyState(
icon = R.drawable.ic_empty_common,
textPrimary = R.string.nothing_found,
textSecondary = R.string.text_search_holder_secondary,
actionStringRes = 0,
),
)
else -> {
val result = ArrayList<ListModel>(list.size + 1)
mangaListMapper.toListModelList(result, list, mode)
when {
error != null -> result += error.toErrorFooter()
hasNext -> result += LoadingFooter()
}
result
}
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
init {
loadList(append = false)
}
override fun onRefresh() {
loadList(append = false)
}
override fun onRetry() {
loadList(append = !mangaList.value.isNullOrEmpty())
}
fun loadNextPage() {
if (hasNextPage.value && listError.value == null) {
loadList(append = true)
}
}
private fun loadList(append: Boolean) {
if (loadingJob?.isActive == true) {
return
}
loadingJob = launchLoadingJob(Dispatchers.Default) {
try {
listError.value = null
val list = repository.getList(
offset = if (append) mangaList.value.sizeOrZero() else 0,
order = null,
filter = MangaListFilter(query = query),
)
val prevList = mangaList.value.orEmpty()
if (!append) {
mangaList.value = list.distinctById()
} else if (list.isNotEmpty()) {
mangaList.value = (prevList + list).distinctById()
}
hasNextPage.value = if (append) {
prevList != mangaList.value
} else {
list.isNotEmpty()
}
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
listError.value = e
}
}
}
}

View File

@@ -34,10 +34,10 @@ import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
import org.koitharu.kotatsu.parsers.model.Manga
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.SearchActivity
import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter
import javax.inject.Inject
@@ -63,7 +63,13 @@ class MultiSearchActivity :
title = viewModel.query
val itemCLickListener = OnListItemClickListener<MultiSearchListModel> { item, view ->
startActivity(SearchActivity.newIntent(view.context, item.source, viewModel.query))
startActivity(
MangaListActivity.newIntent(
view.context,
item.source,
MangaListFilter(query = viewModel.query),
),
)
}
val sizeResolver = DynamicItemSizeResolver(resources, settings, adjustWidth = true)
val selectionDecoration = MangaSelectionDecoration(this)
@@ -125,7 +131,7 @@ class MultiSearchActivity :
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) {
if (!selectionController.onItemClick(manga.id)) {
val intent = MangaListActivity.newIntent(this, setOf(tag))
val intent = MangaListActivity.newIntent(this, manga.source, MangaListFilter(tags = setOf(tag)))
startActivity(intent)
}
}

View File

@@ -89,7 +89,7 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
}
override fun onItemClick(item: SourceCatalogItem.Source, view: View) {
startActivity(MangaListActivity.newIntent(this, item.source))
startActivity(MangaListActivity.newIntent(this, item.source, null))
}
override fun onItemLongClick(item: SourceCatalogItem.Source, view: View): Boolean {

View File

@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways|snap">
<androidx.appcompat.widget.SearchView
android:id="@+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
app:iconifiedByDefault="false"
app:queryBackground="@null"
app:searchHintIcon="@null"
app:searchIcon="@null" />
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -4,16 +4,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/action_search"
android:icon="?actionModeWebSearchDrawable"
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" />
<item
android:id="@+id/action_random"
android:icon="@drawable/ic_dice"
android:orderInCategory="10"
android:title="@string/random"
app:showAsAction="ifRoom" />

View File

@@ -0,0 +1,14 @@
<?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_search"
android:icon="?actionModeWebSearchDrawable"
android:orderInCategory="0"
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" />
</menu>

View File

@@ -726,4 +726,5 @@
<string name="demographic_josei">Josei</string>
<string name="years">Years</string>
<string name="any">Any</string>
<string name="filter_search_warning">This source does not support search with filters. Your filters have been cleared</string>
</resources>