Advanced global search

This commit is contained in:
Koitharu
2025-02-17 17:19:58 +02:00
parent 4ee52e149e
commit 74900970e1
33 changed files with 417 additions and 203 deletions

View File

@@ -10,25 +10,22 @@ import kotlinx.coroutines.sync.withPermit
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.almostEquals
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.search.domain.SearchKind
import org.koitharu.kotatsu.search.domain.SearchV2Helper
import javax.inject.Inject
private const val MAX_PARALLELISM = 4
private const val MATCH_THRESHOLD_DEFAULT = 0.2f
class AlternativesUseCase @Inject constructor(
private val sourcesRepository: MangaSourcesRepository,
private val searchHelperFactory: SearchV2Helper.Factory,
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
suspend operator fun invoke(manga: Manga): Flow<Manga> = invoke(manga, MATCH_THRESHOLD_DEFAULT)
suspend operator fun invoke(manga: Manga, matchThreshold: Float): Flow<Manga> {
suspend operator fun invoke(manga: Manga): Flow<Manga> {
val sources = getSources(manga.source)
if (sources.isEmpty()) {
return emptyFlow()
@@ -36,21 +33,14 @@ class AlternativesUseCase @Inject constructor(
val semaphore = Semaphore(MAX_PARALLELISM)
return channelFlow {
for (source in sources) {
val repository = mangaRepositoryFactory.create(source)
if (!repository.filterCapabilities.isSearchSupported) {
continue
}
launch {
val searchHelper = searchHelperFactory.create(source)
val list = runCatchingCancellable {
semaphore.withPermit {
repository.getList(offset = 0, SortOrder.RELEVANCE, MangaListFilter(query = manga.title))
searchHelper(manga.title, SearchKind.TITLE)?.manga
}
}.getOrDefault(emptyList())
for (item in list) {
if (item.matches(manga, matchThreshold)) {
send(item)
}
}
}.getOrNull()
list?.forEach { send(it) }
}
}
}.map {
@@ -68,18 +58,6 @@ class AlternativesUseCase @Inject constructor(
return result
}
private fun Manga.matches(ref: Manga, threshold: Float): Boolean {
return matchesTitles(title, ref.title, threshold) ||
matchesTitles(title, ref.altTitle, threshold) ||
matchesTitles(altTitle, ref.title, threshold) ||
matchesTitles(altTitle, ref.altTitle, threshold)
}
private fun matchesTitles(a: String?, b: String?, threshold: Float): Boolean {
return !a.isNullOrEmpty() && !b.isNullOrEmpty() && a.almostEquals(b, threshold)
}
private fun MangaSource.priority(ref: MangaSource): Int {
var res = 0
if (this is MangaParserSource && ref is MangaParserSource) {

View File

@@ -35,7 +35,7 @@ class AutoFixUseCase @Inject constructor(
if (seed.isHealthy()) {
return seed to null // no fix required
}
val replacement = alternativesUseCase(seed, matchThreshold = 0.02f)
val replacement = alternativesUseCase(seed)
.filter { it.isHealthy() }
.runningFold<Manga, Manga?>(null) { best, candidate ->
if (best == null || best < candidate) {

View File

@@ -4,12 +4,14 @@ import android.accounts.Account
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.View
import androidx.annotation.CheckResult
import androidx.annotation.UiContext
import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
@@ -17,7 +19,9 @@ import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.findFragment
import androidx.lifecycle.LifecycleOwner
import dagger.hilt.android.EntryPointAccessors
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity
import org.koitharu.kotatsu.browser.BrowserActivity
@@ -25,13 +29,19 @@ import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.MangaSourceInfo
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaListFilter
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.TriStateOption
import org.koitharu.kotatsu.core.ui.dialog.BigButtonsAlertDialog
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.core.util.ext.withArgs
@@ -61,6 +71,7 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.isNullOrEmpty
import org.koitharu.kotatsu.parsers.util.mapToArray
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
@@ -68,6 +79,7 @@ import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.domain.SearchKind
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.multi.SearchActivity
import org.koitharu.kotatsu.settings.SettingsActivity
@@ -94,22 +106,27 @@ class AppRouter private constructor(
constructor(fragment: Fragment) : this(null, fragment)
/** Activities **/
fun openList(source: MangaSource, filter: MangaListFilter?) {
startActivity(listIntent(contextOrNull() ?: return, source, filter))
private val settings: AppSettings by lazy {
EntryPointAccessors.fromApplication<AppRouterEntryPoint>(checkNotNull(contextOrNull())).settings
}
fun openList(tag: MangaTag) = openList(tag.source, MangaListFilter(tags = setOf(tag)))
/** Activities **/
fun openSearch(query: String) {
fun openList(source: MangaSource, filter: MangaListFilter?, sortOrder: SortOrder?) {
startActivity(listIntent(contextOrNull() ?: return, source, filter, sortOrder))
}
fun openList(tag: MangaTag) = openList(tag.source, MangaListFilter(tags = setOf(tag)), null)
fun openSearch(query: String, kind: SearchKind = SearchKind.SIMPLE) {
startActivity(
Intent(contextOrNull() ?: return, SearchActivity::class.java)
.putExtra(KEY_QUERY, query),
.putExtra(KEY_QUERY, query)
.putExtra(KEY_KIND, kind),
)
}
fun openSearch(source: MangaSource, query: String) = openList(source, MangaListFilter(query = query))
fun openSearch(source: MangaSource, query: String) = openList(source, MangaListFilter(query = query), null)
fun openDetails(manga: Manga) {
startActivity(detailsIntent(contextOrNull() ?: return, manga))
@@ -119,6 +136,13 @@ class AppRouter private constructor(
startActivity(detailsIntent(contextOrNull() ?: return, mangaId))
}
fun openDetails(link: Uri) {
startActivity(
Intent(contextOrNull() ?: return, DetailsActivity::class.java)
.setData(link),
)
}
fun openReader(manga: Manga, anchor: View? = null) {
openReader(
ReaderIntent.Builder(contextOrNull() ?: return)
@@ -327,6 +351,25 @@ class AppRouter private constructor(
}.showDistinct()
}
fun showTagDialog(tag: MangaTag) {
buildAlertDialog(contextOrNull() ?: return) {
setTitle(tag.title)
setItems(
arrayOf(
context.getString(R.string.search_on_s, tag.source.getTitle(context)),
context.getString(R.string.search_everywhere),
),
) { _, which ->
when (which) {
0 -> openList(tag)
1 -> openSearch(tag.title, SearchKind.TAG)
}
}
setNegativeButton(R.string.close, null)
setCancelable(true)
}.show()
}
fun showErrorDialog(error: Throwable, url: String? = null) {
ErrorDetailsDialog().withArgs(2) {
putSerializable(KEY_ERROR, error)
@@ -414,6 +457,45 @@ class AppRouter private constructor(
TrackerCategoriesConfigSheet().showDistinct()
}
fun askForDownloadOverMeteredNetwork(onConfirmed: (allow: Boolean) -> Unit) {
val context = contextOrNull() ?: return
when (settings.allowDownloadOnMeteredNetwork) {
TriStateOption.ENABLED -> onConfirmed(true)
TriStateOption.DISABLED -> onConfirmed(false)
TriStateOption.ASK -> {
if (!context.connectivityManager.isActiveNetworkMetered) {
onConfirmed(true)
return
}
val listener = DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
settings.allowDownloadOnMeteredNetwork = TriStateOption.ENABLED
onConfirmed(true)
}
DialogInterface.BUTTON_NEUTRAL -> {
onConfirmed(true)
}
DialogInterface.BUTTON_NEGATIVE -> {
settings.allowDownloadOnMeteredNetwork = TriStateOption.DISABLED
onConfirmed(false)
}
}
}
BigButtonsAlertDialog.Builder(context)
.setIcon(R.drawable.ic_network_cellular)
.setTitle(R.string.download_cellular_confirm)
.setPositiveButton(R.string.allow_always, listener)
.setNeutralButton(R.string.allow_once, listener)
.setNegativeButton(R.string.dont_allow, listener)
.create()
.show()
}
}
}
/** Public utils **/
fun isFilterSupported(): Boolean = when {
@@ -462,6 +544,7 @@ class AppRouter private constructor(
return fragment?.childFragmentManager ?: activity?.supportFragmentManager
}
@UiContext
private fun contextOrNull(): Context? = activity ?: fragment?.context
private fun getLifecycleOwner(): LifecycleOwner? = activity ?: fragment?.viewLifecycleOwner
@@ -510,7 +593,7 @@ class AppRouter private constructor(
fun detailsIntent(context: Context, mangaId: Long) = Intent(context, DetailsActivity::class.java)
.putExtra(KEY_ID, mangaId)
fun listIntent(context: Context, source: MangaSource, filter: MangaListFilter?): Intent =
fun listIntent(context: Context, source: MangaSource, filter: MangaListFilter?, sortOrder: SortOrder?): Intent =
Intent(context, MangaListActivity::class.java)
.setAction(ACTION_MANGA_EXPLORE)
.putExtra(KEY_SOURCE, source.name)
@@ -518,6 +601,9 @@ class AppRouter private constructor(
if (!filter.isNullOrEmpty()) {
putExtra(KEY_FILTER, ParcelableMangaListFilter(filter))
}
if (sortOrder != null) {
putExtra(KEY_SORT_ORDER, sortOrder)
}
}
fun cloudFlareResolveIntent(context: Context, exception: CloudFlareProtectedException): Intent =
@@ -590,12 +676,14 @@ class AppRouter private constructor(
const val KEY_FILTER = "filter"
const val KEY_ID = "id"
const val KEY_INDEX = "index"
const val KEY_KIND = "kind"
const val KEY_LIST_SECTION = "list_section"
const val KEY_MANGA = "manga"
const val KEY_MANGA_LIST = "manga_list"
const val KEY_PAGES = "pages"
const val KEY_QUERY = "query"
const val KEY_READER_MODE = "reader_mode"
const val KEY_SORT_ORDER = "sort_order"
const val KEY_SOURCE = "source"
const val KEY_TAB = "tab"
const val KEY_TITLE = "title"

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core.nav
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.core.prefs.AppSettings
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AppRouterEntryPoint {
val settings: AppSettings
}

View File

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

View File

@@ -6,6 +6,7 @@ import dagger.Reusable
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.UnknownMangaSource
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.util.ext.isHttpUrl
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
@@ -109,4 +110,11 @@ class MangaLinkResolver @Inject constructor(
chapters = null,
source = source,
)
companion object {
fun isValidLink(str: String): Boolean {
return str.isHttpUrl() || str.startsWith("kotatsu://", ignoreCase = true)
}
}
}

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.view.LayoutInflater
import android.widget.CompoundButton.OnCheckedChangeListener
import androidx.annotation.StringRes
import androidx.annotation.UiContext
import androidx.appcompat.app.AlertDialog
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
@@ -17,7 +18,7 @@ import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
import com.google.android.material.R as materialR
inline fun buildAlertDialog(
context: Context,
@UiContext context: Context,
isCentered: Boolean = false,
block: MaterialAlertDialogBuilder.() -> Unit,
): AlertDialog = MaterialAlertDialogBuilder(

View File

@@ -1,58 +0,0 @@
package org.koitharu.kotatsu.core.ui.dialog
import android.content.Context
import android.content.DialogInterface
import androidx.annotation.UiContext
import androidx.core.net.ConnectivityManagerCompat
import dagger.Lazy
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.TriStateOption
import org.koitharu.kotatsu.core.util.ext.connectivityManager
import javax.inject.Inject
class CommonAlertDialogs @Inject constructor(
private val settings: Lazy<AppSettings>,
) {
fun askForDownloadOverMeteredNetwork(
@UiContext context: Context,
onConfirmed: (allow: Boolean) -> Unit
) {
when (settings.get().allowDownloadOnMeteredNetwork) {
TriStateOption.ENABLED -> onConfirmed(true)
TriStateOption.DISABLED -> onConfirmed(false)
TriStateOption.ASK -> {
if (!ConnectivityManagerCompat.isActiveNetworkMetered(context.connectivityManager)) {
onConfirmed(true)
return
}
val listener = DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
settings.get().allowDownloadOnMeteredNetwork = TriStateOption.ENABLED
onConfirmed(true)
}
DialogInterface.BUTTON_NEUTRAL -> {
onConfirmed(true)
}
DialogInterface.BUTTON_NEGATIVE -> {
settings.get().allowDownloadOnMeteredNetwork = TriStateOption.DISABLED
onConfirmed(false)
}
}
}
BigButtonsAlertDialog.Builder(context)
.setIcon(R.drawable.ic_network_cellular)
.setTitle(R.string.download_cellular_confirm)
.setPositiveButton(R.string.allow_always, listener)
.setNeutralButton(R.string.allow_once, listener)
.setNegativeButton(R.string.dont_allow, listener)
.create()
.show()
}
}
}
}

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.core.util.ext
import android.content.Context
import android.database.DatabaseUtils
import androidx.collection.arraySetOf
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.ellipsize
@@ -70,11 +69,4 @@ fun <T> Collection<T>.joinToStringWithLimit(context: Context, limit: Int, transf
}
}
@Deprecated(
"",
ReplaceWith(
"sqlEscapeString(this)",
"android.database.DatabaseUtils.sqlEscapeString",
),
)
fun String.sqlEscape(): String = DatabaseUtils.sqlEscapeString(this)
fun String.isHttpUrl() = startsWith("https://", ignoreCase = true) || startsWith("http://", ignoreCase = true)

View File

@@ -22,12 +22,12 @@ fun Uri.isNetworkUri() = scheme.let {
it == URI_SCHEME_HTTP || it == URI_SCHEME_HTTPS
}
fun File.toZipUri(entryPath: String): Uri = Uri.parse("$URI_SCHEME_ZIP://$absolutePath#$entryPath")
fun File.toZipUri(entryPath: String): Uri = "$URI_SCHEME_ZIP://$absolutePath#$entryPath".toUri()
fun File.toZipUri(entryPath: Path?): Uri =
toZipUri(entryPath?.toString()?.removePrefix(Path.DIRECTORY_SEPARATOR).orEmpty())
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
fun String.toUriOrNull() = if (isEmpty()) null else this.toUri()
fun File.toUri(fragment: String?): Uri = toUri().run {
if (fragment != null) {

View File

@@ -209,7 +209,7 @@ class DetailsActivity :
R.id.textView_source -> {
val manga = viewModel.manga.value ?: return
router.openList(manga.source, null)
router.openList(manga.source, null, null)
}
R.id.textView_local -> {
@@ -255,8 +255,7 @@ class DetailsActivity :
override fun onChipClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag ?: return
// TODO dialog
router.openList(tag)
router.showTagDialog(tag)
}
override fun onContextClick(v: View): Boolean = onLongClick(v)

View File

@@ -19,7 +19,6 @@ import org.koitharu.kotatsu.core.nav.ReaderIntent
import org.koitharu.kotatsu.core.nav.dismissParentDialog
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper
@@ -39,7 +38,6 @@ import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
import org.koitharu.kotatsu.reader.ui.ReaderState
import javax.inject.Inject
import kotlin.math.roundToInt
@AndroidEntryPoint
@@ -49,9 +47,6 @@ class ChaptersFragment :
private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
@Inject
lateinit var commonAlertDialogs: CommonAlertDialogs
private var chaptersAdapter: ChaptersAdapter? = null
private var selectionController: ListSelectionController? = null
@@ -67,7 +62,7 @@ class ChaptersFragment :
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
decoration = ChaptersSelectionDecoration(binding.root.context),
registryOwner = this,
callback = ChaptersSelectionCallback(viewModel, commonAlertDialogs, binding.recyclerViewChapters),
callback = ChaptersSelectionCallback(viewModel, router, binding.recyclerViewChapters),
)
viewModel.isChaptersInGridView.observe(viewLifecycleOwner) { chaptersInGridView ->
binding.recyclerViewChapters.layoutManager = if (chaptersInGridView) {

View File

@@ -8,7 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.list.BaseListSelectionCallback
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.toCollection
@@ -18,7 +18,7 @@ import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
class ChaptersSelectionCallback(
private val viewModel: ChaptersPagesViewModel,
private val commonAlertDialogs: CommonAlertDialogs,
private val router: AppRouter,
recyclerView: RecyclerView,
) : BaseListSelectionCallback(recyclerView) {
@@ -63,10 +63,9 @@ class ChaptersSelectionCallback(
val snapshot = controller.snapshot()
mode?.finish()
if (snapshot.isNotEmpty()) {
commonAlertDialogs.askForDownloadOverMeteredNetwork(
context = recyclerView.context,
onConfirmed = { viewModel.download(snapshot, it) },
)
router.askForDownloadOverMeteredNetwork {
viewModel.download(snapshot, it)
}
}
true
}

View File

@@ -18,9 +18,9 @@ import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.prefs.DownloadFormat
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
import org.koitharu.kotatsu.core.ui.widgets.TwoLinesItemView
import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
@@ -32,9 +32,7 @@ import org.koitharu.kotatsu.core.util.ext.showOrHide
import org.koitharu.kotatsu.databinding.DialogDownloadBinding
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.parsers.util.mapToArray
import org.koitharu.kotatsu.settings.storage.DirectoryModel
import javax.inject.Inject
@AndroidEntryPoint
class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), View.OnClickListener {
@@ -42,9 +40,6 @@ class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), Vie
private val viewModel by viewModels<DownloadDialogViewModel>()
private var optionViews: Array<out TwoLinesItemView>? = null
@Inject
lateinit var commonAlertDialogs: CommonAlertDialogs
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?) =
DialogDownloadBinding.inflate(inflater, container, false)
@@ -104,10 +99,7 @@ class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), Vie
override fun onClick(v: View) {
when (v.id) {
R.id.button_cancel -> dialog?.cancel()
R.id.button_confirm -> commonAlertDialogs.askForDownloadOverMeteredNetwork(
context = context ?: return,
onConfirmed = ::schedule,
)
R.id.button_confirm -> router.askForDownloadOverMeteredNetwork(::schedule)
R.id.textView_more -> {
val binding = viewBinding ?: return

View File

@@ -268,7 +268,7 @@ class DownloadNotificationFactory @AssistedInject constructor(
if (manga != null) {
AppRouter.detailsIntent(context, manga)
} else {
AppRouter.listIntent(context, LocalMangaSource, null)
AppRouter.listIntent(context, LocalMangaSource, null, null)
},
PendingIntent.FLAG_CANCEL_CURRENT,
false,

View File

@@ -121,7 +121,7 @@ class ExploreFragment :
override fun onClick(v: View) {
when (v.id) {
R.id.button_local -> router.openList(LocalMangaSource, null)
R.id.button_local -> router.openList(LocalMangaSource, null, null)
R.id.button_bookmarks -> router.openBookmarks()
R.id.button_more -> router.openSuggestions()
R.id.button_downloads -> router.openDownloads()
@@ -133,7 +133,7 @@ class ExploreFragment :
if (sourceSelectionController?.onItemClick(item.id) == true) {
return
}
router.openList(item.source, null)
router.openList(item.source, null, null)
}
override fun onItemLongClick(item: MangaSourceItem, view: View): Boolean {

View File

@@ -15,6 +15,7 @@ import androidx.appcompat.view.ActionMode
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.view.SoftwareKeyboardControllerCompat
import androidx.core.view.children
import androidx.core.view.inputmethod.EditorInfoCompat
@@ -41,6 +42,7 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.parser.MangaLinkResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.NavItem
import org.koitharu.kotatsu.core.ui.BaseActivity
@@ -61,6 +63,7 @@ import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
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.search.domain.SearchKind
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
@@ -246,11 +249,17 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
router.openDetails(manga)
}
override fun onQueryClick(query: String, submit: Boolean) {
override fun onQueryClick(query: String, kind: SearchKind, submit: Boolean) {
viewBinding.searchView.query = query
if (submit && query.isNotEmpty()) {
router.openSearch(query)
searchSuggestionViewModel.saveQuery(query)
if (kind == SearchKind.SIMPLE && MangaLinkResolver.isValidLink(query)) {
router.openDetails(query.toUri())
} else {
router.openSearch(query, kind)
if (kind != SearchKind.TAG) {
searchSuggestionViewModel.saveQuery(query)
}
}
viewBinding.searchView.post {
closeSearchCallback.handleOnBackPressed()
}
@@ -258,7 +267,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
}
override fun onTagClick(tag: MangaTag) {
router.openList(tag)
router.openSearch(tag.title, SearchKind.TAG)
}
override fun onQueryChanged(query: String) {
@@ -270,7 +279,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
}
override fun onSourceClick(source: MangaSource) {
router.openList(source, null)
router.openList(source, null, null)
}
override fun onSupportActionModeStarted(mode: ActionMode) {

View File

@@ -0,0 +1,6 @@
package org.koitharu.kotatsu.search.domain
enum class SearchKind {
SIMPLE, TITLE, AUTHOR, TAG
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.search.domain
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.SortOrder
data class SearchResults(
val listFilter: MangaListFilter,
val sortOrder: SortOrder,
val manga: List<Manga>,
)

View File

@@ -0,0 +1,124 @@
package org.koitharu.kotatsu.search.domain
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
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.SortOrder
import org.koitharu.kotatsu.parsers.util.almostEquals
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
private const val MATCH_THRESHOLD_DEFAULT = 0.2f
class SearchV2Helper @AssistedInject constructor(
@Assisted private val source: MangaSource,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val dataRepository: MangaDataRepository,
) {
suspend operator fun invoke(query: String, kind: SearchKind): SearchResults? {
val repository = mangaRepositoryFactory.create(source)
val listFilter = repository.getFilter(query, kind) ?: return null
val sortOrder = repository.getSortOrder(kind)
val list = repository.getList(0, sortOrder, listFilter)
if (list.isEmpty()) {
return null
}
val result = list.toMutableList()
result.postFilter(query, kind)
result.sortByRelevance(query, kind)
return SearchResults(listFilter = listFilter, sortOrder = sortOrder, manga = result)
}
private suspend fun MangaRepository.getFilter(query: String, kind: SearchKind): MangaListFilter? = when (kind) {
SearchKind.SIMPLE,
SearchKind.TITLE,
SearchKind.AUTHOR -> if (filterCapabilities.isSearchSupported) { // TODO author support
MangaListFilter(query = query)
} else {
null
}
SearchKind.TAG -> {
val tags = this@SearchV2Helper.dataRepository.findTags(this.source) + runCatchingCancellable {
this@getFilter.getFilterOptions().availableTags
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrDefault(emptySet())
val tag = tags.find { x -> x.title.equals(query, ignoreCase = true) }
if (tag != null) {
MangaListFilter(tags = setOf(tag))
} else {
null
}
}
}
private fun MutableList<Manga>.postFilter(query: String, kind: SearchKind) {
when (kind) {
SearchKind.TITLE -> retainAll { m ->
m.matches(query, MATCH_THRESHOLD_DEFAULT)
}
SearchKind.AUTHOR -> retainAll { m ->
m.author.isNullOrEmpty() || m.author.equals(query, ignoreCase = true)
}
SearchKind.SIMPLE, // no filtering expected
SearchKind.TAG -> Unit
}
}
private fun MutableList<Manga>.sortByRelevance(query: String, kind: SearchKind) {
when (kind) {
SearchKind.SIMPLE,
SearchKind.TITLE -> sortBy { m ->
minOf(m.title.levenshteinDistance(query), m.altTitle?.levenshteinDistance(query) ?: Int.MAX_VALUE)
}
SearchKind.AUTHOR -> sortByDescending { m ->
m.author?.equals(query, ignoreCase = true) == true
}
SearchKind.TAG -> sortByDescending { m ->
m.tags.any { tag -> tag.title.equals(query, ignoreCase = true) }
}
}
}
private fun MangaRepository.getSortOrder(kind: SearchKind): SortOrder {
val preferred: SortOrder = when (kind) {
SearchKind.SIMPLE,
SearchKind.TITLE,
SearchKind.AUTHOR -> SortOrder.RELEVANCE
SearchKind.TAG -> SortOrder.POPULARITY
}
return if (preferred in sortOrders) {
preferred
} else {
defaultSortOrder
}
}
private fun Manga.matches(query: String, threshold: Float): Boolean {
return matchesTitles(title, query, threshold) || matchesTitles(altTitle, query, threshold)
}
private fun matchesTitles(a: String?, b: String?, threshold: Float): Boolean {
return !a.isNullOrEmpty() && !b.isNullOrEmpty() && a.almostEquals(b, threshold)
}
@AssistedFactory
interface Factory {
fun create(source: MangaSource): SearchV2Helper
}
}

View File

@@ -31,6 +31,7 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.util.ViewBadge
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
@@ -44,6 +45,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.SortOrder
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import kotlin.math.absoluteValue
import com.google.android.material.R as materialR
@@ -67,6 +69,7 @@ class MangaListActivity :
super.onCreate(savedInstanceState)
setContentView(ActivityMangaListBinding.inflate(layoutInflater))
val filter = intent.getParcelableExtraCompat<ParcelableMangaListFilter>(AppRouter.KEY_FILTER)?.filter
val sortOrder = intent.getSerializableExtraCompat<SortOrder>(AppRouter.KEY_SORT_ORDER)
source = MangaSource(intent.getStringExtra(AppRouter.KEY_SOURCE))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (viewBinding.containerFilterHeader != null) {
@@ -74,7 +77,7 @@ class MangaListActivity :
}
viewBinding.buttonOrder?.setOnClickListener(this)
title = source.getTitle(this)
initList(source, filter)
initList(source, filter, sortOrder)
}
override fun isNsfwContent(): Flow<Boolean> = flowOf(source.isNsfw())
@@ -112,7 +115,7 @@ class MangaListActivity :
fun hidePreview() = setSideFragment(FilterSheetFragment::class.java, null)
private fun initList(source: MangaSource, filter: MangaListFilter?) {
private fun initList(source: MangaSource, filter: MangaListFilter?, sortOrder: SortOrder?) {
val fm = supportFragmentManager
val existingFragment = fm.findFragmentById(R.id.container)
if (existingFragment is FilterCoordinator.Owner) {
@@ -127,8 +130,8 @@ class MangaListActivity :
}
replace(R.id.container, fragment)
runOnCommit { initFilter(fragment) }
if (filter != null) {
runOnCommit(ApplyFilterRunnable(fragment, filter))
if (filter != null || sortOrder != null) {
runOnCommit(ApplyFilterRunnable(fragment, filter, sortOrder))
}
}
}
@@ -182,11 +185,17 @@ class MangaListActivity :
private class ApplyFilterRunnable(
private val filterOwner: FilterCoordinator.Owner,
private val filter: MangaListFilter,
private val filter: MangaListFilter?,
private val sortOrder: SortOrder?,
) : Runnable {
override fun run() {
filterOwner.filterCoordinator.set(filter)
if (sortOrder != null) {
filterOwner.filterCoordinator.setSortOrder(sortOrder)
}
if (filter != null) {
filterOwner.filterCoordinator.set(filter)
}
}
}
}

View File

@@ -32,6 +32,7 @@ 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.MangaTag
import org.koitharu.kotatsu.search.domain.SearchKind
import org.koitharu.kotatsu.search.ui.multi.adapter.SearchAdapter
import javax.inject.Inject
@@ -53,10 +54,25 @@ class SearchActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySearchBinding.inflate(layoutInflater))
title = viewModel.query
title = when (viewModel.kind) {
SearchKind.SIMPLE,
SearchKind.TITLE -> viewModel.query
SearchKind.AUTHOR -> getString(
R.string.inline_preference_pattern,
getString(R.string.author),
viewModel.query,
)
SearchKind.TAG -> getString(R.string.inline_preference_pattern, getString(R.string.genre), viewModel.query)
}
val itemClickListener = OnListItemClickListener<SearchResultsListModel> { item, view ->
router.openSearch(item.source, viewModel.query)
if (item.listFilter == null) {
router.openSearch(item.source, viewModel.query)
} else {
router.openList(item.source, item.listFilter, item.sortOrder)
}
}
val sizeResolver = DynamicItemSizeResolver(resources, settings, adjustWidth = true)
val selectionDecoration = MangaSelectionDecoration(this)

View File

@@ -6,11 +6,15 @@ import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
data class SearchResultsListModel(
@StringRes val titleResId: Int,
val source: MangaSource,
val listFilter: MangaListFilter?,
val sortOrder: SortOrder?,
val hasMore: Boolean,
val list: List<MangaListModel>,
val error: Throwable?,

View File

@@ -26,7 +26,6 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.model.UnknownMangaSource
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@@ -43,6 +42,8 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.search.domain.SearchKind
import org.koitharu.kotatsu.search.domain.SearchV2Helper
import javax.inject.Inject
private const val MAX_PARALLELISM = 4
@@ -52,7 +53,7 @@ private const val MIN_HAS_MORE_ITEMS = 8
class SearchViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val mangaListMapper: MangaListMapper,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val searchHelperFactory: SearchV2Helper.Factory,
private val sourcesRepository: MangaSourcesRepository,
private val historyRepository: HistoryRepository,
private val localMangaRepository: LocalMangaRepository,
@@ -60,6 +61,7 @@ class SearchViewModel @Inject constructor(
) : BaseViewModel() {
val query = savedStateHandle.get<String>(AppRouter.KEY_QUERY).orEmpty()
val kind = savedStateHandle.get<SearchKind>(AppRouter.KEY_KIND) ?: SearchKind.SIMPLE
private val retryCounter = MutableStateFlow(0)
private val listData = retryCounter.flatMapLatest {
@@ -115,35 +117,40 @@ class SearchViewModel @Inject constructor(
return@channelFlow
}
val semaphore = Semaphore(MAX_PARALLELISM)
sources.mapNotNull { source ->
val repository = mangaRepositoryFactory.create(source)
if (!repository.filterCapabilities.isSearchSupported) {
null
} else {
launch {
val item = runCatchingCancellable {
semaphore.withPermit {
mangaListMapper.toListModelList(
manga = repository.getList(offset = 0, null, MangaListFilter(query = q)),
sources.map { source ->
launch {
val item = runCatchingCancellable {
semaphore.withPermit {
val searchHelper = searchHelperFactory.create(source)
searchHelper(query, kind)
}
}.fold(
onSuccess = { result ->
if (result == null || result.manga.isEmpty()) {
null
} else {
val list = mangaListMapper.toListModelList(
manga = result.manga,
mode = ListMode.GRID,
)
SearchResultsListModel(
titleResId = 0,
source = source,
hasMore = list.size > MIN_HAS_MORE_ITEMS,
list = list,
error = null,
listFilter = result.listFilter,
sortOrder = result.sortOrder,
)
}
}.fold(
onSuccess = { list ->
if (list.isEmpty()) {
null
} else {
SearchResultsListModel(0, source, list.size > MIN_HAS_MORE_ITEMS, list, null)
}
},
onFailure = { error ->
error.printStackTraceDebug()
SearchResultsListModel(0, source, true, emptyList(), error)
},
)
if (item != null) {
send(item)
}
},
onFailure = { error ->
error.printStackTraceDebug()
SearchResultsListModel(0, source, null, null, true, emptyList(), error)
},
)
if (item != null) {
send(item)
}
}
}.joinAll()
@@ -163,6 +170,8 @@ class SearchViewModel @Inject constructor(
hasMore = false,
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
error = null,
listFilter = null,
sortOrder = null,
)
} else {
null
@@ -175,6 +184,8 @@ class SearchViewModel @Inject constructor(
hasMore = false,
list = emptyList(),
error = error,
listFilter = null,
sortOrder = null,
)
},
)
@@ -192,6 +203,8 @@ class SearchViewModel @Inject constructor(
hasMore = false,
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
error = null,
listFilter = null,
sortOrder = null,
)
} else {
null
@@ -204,6 +217,8 @@ class SearchViewModel @Inject constructor(
hasMore = false,
list = emptyList(),
error = error,
listFilter = null,
sortOrder = null,
)
},
)
@@ -221,6 +236,8 @@ class SearchViewModel @Inject constructor(
hasMore = result.size > MIN_HAS_MORE_ITEMS,
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
error = null,
listFilter = null,
sortOrder = null,
)
} else {
null
@@ -233,6 +250,8 @@ class SearchViewModel @Inject constructor(
hasMore = true,
list = emptyList(),
error = error,
listFilter = null,
sortOrder = null,
)
},
)

View File

@@ -3,12 +3,13 @@ package org.koitharu.kotatsu.search.ui.suggestion
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.search.domain.SearchKind
interface SearchSuggestionListener {
fun onMangaClick(manga: Manga)
fun onQueryClick(query: String, submit: Boolean)
fun onQueryClick(query: String, kind: SearchKind, submit: Boolean)
fun onQueryChanged(query: String)

View File

@@ -4,6 +4,7 @@ import android.view.View
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionQueryHintBinding
import org.koitharu.kotatsu.search.domain.SearchKind
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
@@ -14,7 +15,7 @@ fun searchSuggestionAuthorAD(
) {
val viewClickListener = View.OnClickListener { _ ->
listener.onQueryClick(item.name, true)
listener.onQueryClick(item.name, SearchKind.AUTHOR, true)
}
binding.root.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_user, 0, 0, 0)

View File

@@ -4,23 +4,25 @@ import android.view.View
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionQueryBinding
import org.koitharu.kotatsu.search.domain.SearchKind
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
fun searchSuggestionQueryAD(
listener: SearchSuggestionListener,
) = adapterDelegateViewBinding<SearchSuggestionItem.RecentQuery, SearchSuggestionItem, ItemSearchSuggestionQueryBinding>(
{ inflater, parent -> ItemSearchSuggestionQueryBinding.inflate(inflater, parent, false) }
) {
) =
adapterDelegateViewBinding<SearchSuggestionItem.RecentQuery, SearchSuggestionItem, ItemSearchSuggestionQueryBinding>(
{ inflater, parent -> ItemSearchSuggestionQueryBinding.inflate(inflater, parent, false) },
) {
val viewClickListener = View.OnClickListener { v ->
listener.onQueryClick(item.query, v.id != R.id.button_complete)
val viewClickListener = View.OnClickListener { v ->
listener.onQueryClick(item.query, SearchKind.SIMPLE, v.id != R.id.button_complete)
}
binding.root.setOnClickListener(viewClickListener)
binding.buttonComplete.setOnClickListener(viewClickListener)
bind {
binding.textViewTitle.text = item.query
}
}
binding.root.setOnClickListener(viewClickListener)
binding.buttonComplete.setOnClickListener(viewClickListener)
bind {
binding.textViewTitle.text = item.query
}
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.search.ui.suggestion.adapter
import android.view.View
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionQueryHintBinding
import org.koitharu.kotatsu.search.domain.SearchKind
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
@@ -13,7 +14,7 @@ fun searchSuggestionQueryHintAD(
) {
val viewClickListener = View.OnClickListener { _ ->
listener.onQueryClick(item.query, true)
listener.onQueryClick(item.query, SearchKind.SIMPLE, true)
}
binding.root.setOnClickListener(viewClickListener)

View File

@@ -21,6 +21,7 @@ import androidx.core.content.ContextCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.drawableEnd
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.search.domain.SearchKind
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import com.google.android.material.R as materialR
@@ -66,7 +67,7 @@ class SearchEditText @JvmOverloads constructor(
&& query.isNotEmpty()
) {
cancelLongPress()
searchSuggestionListener?.onQueryClick(query, submit = true)
searchSuggestionListener?.onQueryClick(query, SearchKind.SIMPLE, submit = true)
clearFocus()
return true
}
@@ -76,7 +77,7 @@ class SearchEditText @JvmOverloads constructor(
override fun onEditorAction(actionCode: Int) {
super.onEditorAction(actionCode)
if (actionCode == EditorInfo.IME_ACTION_SEARCH) {
searchSuggestionListener?.onQueryClick(query, submit = true)
searchSuggestionListener?.onQueryClick(query, SearchKind.SIMPLE, submit = true)
}
}

View File

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

View File

@@ -6,6 +6,7 @@ import android.content.Context
import androidx.annotation.WorkerThread
import dagger.hilt.android.qualifiers.ApplicationContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.isHttpUrl
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import javax.inject.Inject
@@ -39,10 +40,10 @@ class SyncSettings(
companion object {
private fun String.withHttpSchema(): String = if (!startsWith("http://") && !startsWith("https://")) {
"http://$this"
} else {
private fun String.withHttpSchema(): String = if (isHttpUrl()) {
this
} else {
"http://$this"
}
const val KEY_SYNC_URL = "host"

View File

@@ -13,6 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.isHttpUrl
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.PreferenceDialogAutocompletetextviewBinding
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
@@ -66,7 +67,7 @@ class SyncHostDialogFragment : AlertDialogFragment<PreferenceDialogAutocompletet
DialogInterface.BUTTON_POSITIVE -> {
val result = requireViewBinding().edit.text?.toString().orEmpty()
var scheme = ""
if (!result.startsWith("https://") && !result.startsWith("http://")) {
if (!result.isHttpUrl()) {
scheme = "http://"
}
syncSettings.syncUrl = "$scheme$result"

View File

@@ -801,4 +801,5 @@
<string name="screen_rotation_locked">Screen rotation has been locked</string>
<string name="screen_rotation_unlocked">Screen rotation has been unlocked</string>
<string name="badges_in_lists">Badges in lists</string>
<string name="search_everywhere">Search everywhere</string>
</resources>