Configure search suggestions
This commit is contained in:
@@ -32,6 +32,7 @@ import org.koitharu.kotatsu.parsers.util.mapToSet
|
|||||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
|
import java.util.EnumSet
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -220,6 +221,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getBoolean(KEY_APP_PASSWORD_NUMERIC, false)
|
get() = prefs.getBoolean(KEY_APP_PASSWORD_NUMERIC, false)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_APP_PASSWORD_NUMERIC, value) }
|
set(value) = prefs.edit { putBoolean(KEY_APP_PASSWORD_NUMERIC, value) }
|
||||||
|
|
||||||
|
val searchSuggestionTypes: Set<SearchSuggestionType>
|
||||||
|
get() = prefs.getStringSet(KEY_SEARCH_SUGGESTION_TYPES, null)?.let { stringSet ->
|
||||||
|
stringSet.mapNotNullTo(EnumSet.noneOf(SearchSuggestionType::class.java)) { x ->
|
||||||
|
enumValueOf<SearchSuggestionType>(x)
|
||||||
|
}
|
||||||
|
} ?: EnumSet.allOf(SearchSuggestionType::class.java)
|
||||||
|
|
||||||
val isLoggingEnabled: Boolean
|
val isLoggingEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
|
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
|
||||||
|
|
||||||
@@ -675,5 +683,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_APP_UPDATE = "app_update"
|
const val KEY_APP_UPDATE = "app_update"
|
||||||
const val KEY_APP_TRANSLATION = "about_app_translation"
|
const val KEY_APP_TRANSLATION = "about_app_translation"
|
||||||
const val KEY_FEED_HEADER = "feed_header"
|
const val KEY_FEED_HEADER = "feed_header"
|
||||||
|
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.koitharu.kotatsu.core.prefs
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
|
enum class SearchSuggestionType(
|
||||||
|
@StringRes val titleResId: Int,
|
||||||
|
) {
|
||||||
|
|
||||||
|
GENRES(R.string.genres),
|
||||||
|
QUERIES_RECENT(R.string.recent_queries),
|
||||||
|
QUERIES_SUGGEST(R.string.suggested_queries),
|
||||||
|
MANGA(R.string.content_type_manga),
|
||||||
|
SOURCES(R.string.remote_sources),
|
||||||
|
}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util
|
|
||||||
|
|
||||||
import androidx.collection.ArrayMap
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
|
|
||||||
@Deprecated("", replaceWith = ReplaceWith("CompositeMutex2"))
|
|
||||||
class CompositeMutex<T : Any> : Set<T> {
|
|
||||||
|
|
||||||
private val state = ArrayMap<T, MutableStateFlow<Boolean>>()
|
|
||||||
private val mutex = Mutex()
|
|
||||||
|
|
||||||
override val size: Int
|
|
||||||
get() = state.size
|
|
||||||
|
|
||||||
override fun contains(element: T): Boolean {
|
|
||||||
return state.containsKey(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun containsAll(elements: Collection<T>): Boolean {
|
|
||||||
return elements.all { x -> state.containsKey(x) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEmpty(): Boolean {
|
|
||||||
return state.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun iterator(): Iterator<T> {
|
|
||||||
return state.keys.iterator()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun lock(element: T) {
|
|
||||||
while (coroutineContext.isActive) {
|
|
||||||
waitForRemoval(element)
|
|
||||||
mutex.withLock {
|
|
||||||
if (state[element] == null) {
|
|
||||||
state[element] = MutableStateFlow(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unlock(element: T) {
|
|
||||||
checkNotNull(state.remove(element)) {
|
|
||||||
"CompositeMutex is not locked for $element"
|
|
||||||
}.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun waitForRemoval(element: T) {
|
|
||||||
val flow = state[element] ?: return
|
|
||||||
flow.first { it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util
|
|
||||||
|
|
||||||
class CompositeRunnable(
|
|
||||||
private val children: List<Runnable>,
|
|
||||||
) : Runnable, Collection<Runnable> by children {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
for (child in children) {
|
|
||||||
child.run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.util
|
|||||||
import androidx.collection.ArrayMap
|
import androidx.collection.ArrayMap
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
|
||||||
class CompositeMutex2<T : Any> : Set<T> {
|
class MultiMutex<T : Any> : Set<T> {
|
||||||
|
|
||||||
private val delegates = ArrayMap<T, Mutex>()
|
private val delegates = ArrayMap<T, Mutex>()
|
||||||
|
|
||||||
@@ -68,3 +68,5 @@ fun <T> Iterable<T>.sortedWithSafe(comparator: Comparator<in T>): List<T> = try
|
|||||||
toList()
|
toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Collection<*>?.sizeOrZero() = if (this == null) 0 else size
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
inline fun <T : Fragment> T.withArgs(size: Int, block: Bundle.() -> Unit): T {
|
inline fun <T : Fragment> T.withArgs(size: Int, block: Bundle.() -> Unit): T {
|
||||||
val b = Bundle(size)
|
val b = Bundle(size)
|
||||||
@@ -33,26 +28,6 @@ fun Fragment.addMenuProvider(provider: MenuProvider) {
|
|||||||
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainThread
|
|
||||||
suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner {
|
|
||||||
val liveData = viewLifecycleOwnerLiveData
|
|
||||||
liveData.value?.let { return it }
|
|
||||||
return suspendCancellableCoroutine { cont ->
|
|
||||||
val observer = object : Observer<LifecycleOwner?> {
|
|
||||||
override fun onChanged(value: LifecycleOwner?) {
|
|
||||||
if (value != null) {
|
|
||||||
liveData.removeObserver(this)
|
|
||||||
cont.resume(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
liveData.observeForever(observer)
|
|
||||||
cont.invokeOnCancellation {
|
|
||||||
liveData.removeObserver(observer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun DialogFragment.showDistinct(fm: FragmentManager, tag: String) {
|
fun DialogFragment.showDistinct(fm: FragmentManager, tag: String) {
|
||||||
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
|
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
|
||||||
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
|
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.util.CompositeRunnable
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
||||||
if (obj == null || !isInstance(obj)) {
|
if (obj == null || !isInstance(obj)) {
|
||||||
@@ -9,15 +7,3 @@ fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
|||||||
}
|
}
|
||||||
return obj as T
|
return obj as T
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CompositeRunnable */
|
|
||||||
|
|
||||||
operator fun Runnable.plus(other: Runnable): Runnable {
|
|
||||||
val list = ArrayList<Runnable>(this.size + other.size)
|
|
||||||
if (this is CompositeRunnable) list.addAll(this) else list.add(this)
|
|
||||||
if (other is CompositeRunnable) list.addAll(other) else list.add(other)
|
|
||||||
return CompositeRunnable(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val Runnable.size: Int
|
|
||||||
get() = if (this is CompositeRunnable) size else 1
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
|
||||||
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.text.StaticLayout
|
|
||||||
import androidx.core.graphics.withTranslation
|
|
||||||
|
|
||||||
fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
|
|
||||||
canvas.withTranslation(x, y) {
|
|
||||||
draw(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.createViewModelLazy
|
import androidx.fragment.app.createViewModelLazy
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelStore
|
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
@@ -19,7 +17,3 @@ inline fun <reified VM : ViewModel> Fragment.parentFragmentViewModels(
|
|||||||
extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras },
|
extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras },
|
||||||
factoryProducer = factoryProducer ?: { requireParentFragment().defaultViewModelProviderFactory },
|
factoryProducer = factoryProducer ?: { requireParentFragment().defaultViewModelProviderFactory },
|
||||||
)
|
)
|
||||||
|
|
||||||
val ViewModelStore.values: Collection<ViewModel>
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
get() = this.keys().mapNotNull { get(it) }
|
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ suspend fun WorkManager.getWorkSpec(id: UUID): WorkSpec? = suspendCoroutine { co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
suspend fun WorkManager.getWorkInputData(id: UUID): Data? = getWorkSpec(id)?.input
|
suspend fun WorkManager.getWorkInputData(id: UUID): Data? = getWorkSpec(id)?.input
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.core.model.isLocal
|
|||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
||||||
import org.koitharu.kotatsu.core.util.CompositeMutex2
|
import org.koitharu.kotatsu.core.util.MultiMutex
|
||||||
import org.koitharu.kotatsu.core.util.ext.children
|
import org.koitharu.kotatsu.core.util.ext.children
|
||||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||||
import org.koitharu.kotatsu.core.util.ext.filterWith
|
import org.koitharu.kotatsu.core.util.ext.filterWith
|
||||||
@@ -50,7 +50,7 @@ class LocalMangaRepository @Inject constructor(
|
|||||||
) : MangaRepository {
|
) : MangaRepository {
|
||||||
|
|
||||||
override val source = MangaSource.LOCAL
|
override val source = MangaSource.LOCAL
|
||||||
private val locks = CompositeMutex2<Long>()
|
private val locks = MultiMutex<Long>()
|
||||||
|
|
||||||
override val isMultipleTagsSupported: Boolean = true
|
override val isMultipleTagsSupported: Boolean = true
|
||||||
override val isTagsExclusionSupported: Boolean = true
|
override val isTagsExclusionSupported: Boolean = true
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import org.koitharu.kotatsu.core.util.ext.call
|
|||||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
|
||||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||||
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
|
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
@@ -432,7 +433,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
branch = chapter.branch,
|
branch = chapter.branch,
|
||||||
chapterName = chapter.name,
|
chapterName = chapter.name,
|
||||||
chapterNumber = chapterIndex + 1,
|
chapterNumber = chapterIndex + 1,
|
||||||
chaptersTotal = m.chapters[chapter.branch]?.size ?: 0,
|
chaptersTotal = m.chapters[chapter.branch].sizeOrZero(),
|
||||||
totalPages = chaptersLoader.getPagesCount(chapter.id),
|
totalPages = chaptersLoader.getPagesCount(chapter.id),
|
||||||
currentPage = state.page,
|
currentPage = state.page,
|
||||||
isSliderEnabled = settings.isReaderSliderEnabled,
|
isSliderEnabled = settings.isReaderSliderEnabled,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.config
|
package org.koitharu.kotatsu.reader.ui.config
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import androidx.activity.result.ActivityResultCallback
|
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
@@ -25,12 +23,12 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
|
|||||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||||
import org.koitharu.kotatsu.core.util.ScreenOrientationHelper
|
import org.koitharu.kotatsu.core.util.ScreenOrientationHelper
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.findParentCallback
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
|
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
|
||||||
import org.koitharu.kotatsu.reader.ui.PageSaveContract
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
|
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
|
||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||||
@@ -100,7 +98,7 @@ class ReaderConfigSheet :
|
|||||||
binding.sliderTimer.valueTo,
|
binding.sliderTimer.valueTo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
findCallback()?.run {
|
findParentCallback(Callback::class.java)?.run {
|
||||||
binding.switchScrollTimer.isChecked = isAutoScrollEnabled
|
binding.switchScrollTimer.isChecked = isAutoScrollEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +111,7 @@ class ReaderConfigSheet :
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.button_save_page -> {
|
R.id.button_save_page -> {
|
||||||
findCallback()?.onSavePageClick() ?: return
|
findParentCallback(Callback::class.java)?.onSavePageClick() ?: return
|
||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +130,7 @@ class ReaderConfigSheet :
|
|||||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||||
when (buttonView.id) {
|
when (buttonView.id) {
|
||||||
R.id.switch_scroll_timer -> {
|
R.id.switch_scroll_timer -> {
|
||||||
findCallback()?.isAutoScrollEnabled = isChecked
|
findParentCallback(Callback::class.java)?.isAutoScrollEnabled = isChecked
|
||||||
requireViewBinding().layoutTimer.isVisible = isChecked
|
requireViewBinding().layoutTimer.isVisible = isChecked
|
||||||
requireViewBinding().sliderTimer.isVisible = isChecked
|
requireViewBinding().sliderTimer.isVisible = isChecked
|
||||||
}
|
}
|
||||||
@@ -143,7 +141,7 @@ class ReaderConfigSheet :
|
|||||||
|
|
||||||
R.id.switch_double_reader -> {
|
R.id.switch_double_reader -> {
|
||||||
settings.isReaderDoubleOnLandscape = isChecked
|
settings.isReaderDoubleOnLandscape = isChecked
|
||||||
findCallback()?.onDoubleModeChanged(isChecked)
|
findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +165,7 @@ class ReaderConfigSheet :
|
|||||||
if (newMode == mode) {
|
if (newMode == mode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
findCallback()?.onReaderModeChanged(newMode) ?: return
|
findParentCallback(Callback::class.java)?.onReaderModeChanged(newMode) ?: return
|
||||||
mode = newMode
|
mode = newMode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,10 +194,6 @@ class ReaderConfigSheet :
|
|||||||
switch.setOnCheckedChangeListener(this)
|
switch.setOnCheckedChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findCallback(): Callback? {
|
|
||||||
return (parentFragment as? Callback) ?: (activity as? Callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
|
|
||||||
var isAutoScrollEnabled: Boolean
|
var isAutoScrollEnabled: Boolean
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
|||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.core.util.ext.require
|
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.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||||
@@ -134,7 +135,7 @@ open class RemoteListViewModel @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
listError.value = null
|
listError.value = null
|
||||||
val list = repository.getList(
|
val list = repository.getList(
|
||||||
offset = if (append) mangaList.value?.size ?: 0 else 0,
|
offset = if (append) mangaList.value.sizeOrZero() else 0,
|
||||||
filter = filterState,
|
filter = filterState,
|
||||||
)
|
)
|
||||||
val prevList = mangaList.value.orEmpty()
|
val prevList = mangaList.value.orEmpty()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.model.distinctById
|
|||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.ext.require
|
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.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
@@ -103,7 +104,7 @@ class SearchViewModel @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
listError.value = null
|
listError.value = null
|
||||||
val list = repository.getList(
|
val list = repository.getList(
|
||||||
offset = if (append) mangaList.value?.size ?: 0 else 0,
|
offset = if (append) mangaList.value.sizeOrZero() else 0,
|
||||||
filter = MangaListFilter.Search(query),
|
filter = MangaListFilter.Search(query),
|
||||||
)
|
)
|
||||||
val prevList = mangaList.value.orEmpty()
|
val prevList = mangaList.value.orEmpty()
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ import kotlinx.coroutines.flow.mapLatest
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.SearchSuggestionType
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
|
||||||
import org.koitharu.kotatsu.core.util.ext.toEnumSet
|
import org.koitharu.kotatsu.core.util.ext.toEnumSet
|
||||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
@@ -100,9 +103,10 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
suggestionJob = combine(
|
suggestionJob = combine(
|
||||||
query.debounce(DEBOUNCE_TIMEOUT),
|
query.debounce(DEBOUNCE_TIMEOUT),
|
||||||
sourcesRepository.observeEnabledSources().map { it.toEnumSet() },
|
sourcesRepository.observeEnabledSources().map { it.toEnumSet() },
|
||||||
::Pair,
|
settings.observeAsFlow(AppSettings.KEY_SEARCH_SUGGESTION_TYPES) { searchSuggestionTypes },
|
||||||
).mapLatest { (searchQuery, enabledSources) ->
|
::Triple,
|
||||||
buildSearchSuggestion(searchQuery, enabledSources)
|
).mapLatest { (searchQuery, enabledSources, types) ->
|
||||||
|
buildSearchSuggestion(searchQuery, enabledSources, types)
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
.onEach {
|
.onEach {
|
||||||
suggestion.value = it
|
suggestion.value = it
|
||||||
@@ -112,36 +116,49 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
private suspend fun buildSearchSuggestion(
|
private suspend fun buildSearchSuggestion(
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
enabledSources: Set<MangaSource>,
|
enabledSources: Set<MangaSource>,
|
||||||
|
types: Set<SearchSuggestionType>,
|
||||||
): List<SearchSuggestionItem> = coroutineScope {
|
): List<SearchSuggestionItem> = coroutineScope {
|
||||||
val queriesDeferred = async {
|
val queriesDeferred = if (SearchSuggestionType.QUERIES_RECENT in types) {
|
||||||
repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS)
|
async { repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
val hintsDeferred = async {
|
val hintsDeferred = if (SearchSuggestionType.QUERIES_SUGGEST in types) {
|
||||||
repository.getQueryHintSuggestion(searchQuery, MAX_HINTS_ITEMS)
|
async { repository.getQueryHintSuggestion(searchQuery, MAX_HINTS_ITEMS) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
val tagsDeferred = async {
|
val tagsDeferred = if (SearchSuggestionType.GENRES in types) {
|
||||||
repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null)
|
async { repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
val mangaDeferred = async {
|
val mangaDeferred = if (SearchSuggestionType.MANGA in types) {
|
||||||
repository.getMangaSuggestion(searchQuery, MAX_MANGA_ITEMS, null)
|
async { repository.getMangaSuggestion(searchQuery, MAX_MANGA_ITEMS, null) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val sources = if (SearchSuggestionType.SOURCES in types) {
|
||||||
|
repository.getSourcesSuggestion(searchQuery, MAX_SOURCES_ITEMS)
|
||||||
|
} else {
|
||||||
|
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()
|
||||||
val hints = hintsDeferred.await()
|
val hints = hintsDeferred?.await()
|
||||||
|
|
||||||
buildList(queries.size + sources.size + hints.size + 2) {
|
buildList(queries.sizeOrZero() + sources.sizeOrZero() + hints.sizeOrZero() + 2) {
|
||||||
if (tags.isNotEmpty()) {
|
if (!tags.isNullOrEmpty()) {
|
||||||
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
||||||
}
|
}
|
||||||
if (mangaList.isNotEmpty()) {
|
if (!mangaList.isNullOrEmpty()) {
|
||||||
add(SearchSuggestionItem.MangaList(mangaList))
|
add(SearchSuggestionItem.MangaList(mangaList))
|
||||||
}
|
}
|
||||||
sources.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
|
sources?.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
|
||||||
queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
||||||
hints.mapTo(this) { SearchSuggestionItem.Hint(it) }
|
hints?.mapTo(this) { SearchSuggestionItem.Hint(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.view.postDelayed
|
import androidx.core.view.postDelayed
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.preference.MultiSelectListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.TwoStatePreference
|
import androidx.preference.TwoStatePreference
|
||||||
import androidx.preference.forEach
|
import androidx.preference.forEach
|
||||||
@@ -21,6 +22,7 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.SearchSuggestionType
|
||||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||||
@@ -29,9 +31,12 @@ import org.koitharu.kotatsu.core.util.ext.observe
|
|||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
import org.koitharu.kotatsu.parsers.util.names
|
||||||
import org.koitharu.kotatsu.settings.backup.BackupDialogFragment
|
import org.koitharu.kotatsu.settings.backup.BackupDialogFragment
|
||||||
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
|
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
|
||||||
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
|
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
|
||||||
|
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -87,6 +92,12 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
|
|||||||
findPreference<StorageUsagePreference>("storage_usage")?.let { pref ->
|
findPreference<StorageUsagePreference>("storage_usage")?.let { pref ->
|
||||||
viewModel.storageUsage.observe(viewLifecycleOwner, pref)
|
viewModel.storageUsage.observe(viewLifecycleOwner, pref)
|
||||||
}
|
}
|
||||||
|
findPreference<MultiSelectListPreference>(AppSettings.KEY_SEARCH_SUGGESTION_TYPES)?.let { pref ->
|
||||||
|
pref.entryValues = SearchSuggestionType.entries.names()
|
||||||
|
pref.entries = SearchSuggestionType.entries.map { pref.context.getString(it.titleResId) }.toTypedArray()
|
||||||
|
pref.summaryProvider = MultiSummaryProvider(R.string.none)
|
||||||
|
pref.values = settings.searchSuggestionTypes.mapToSet { it.name }
|
||||||
|
}
|
||||||
viewModel.loadingKeys.observe(viewLifecycleOwner) { keys ->
|
viewModel.loadingKeys.observe(viewLifecycleOwner) { keys ->
|
||||||
preferenceScreen.forEach { pref ->
|
preferenceScreen.forEach { pref ->
|
||||||
pref.isEnabled = pref.key !in keys
|
pref.isEnabled = pref.key !in keys
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
|||||||
import org.koitharu.kotatsu.core.util.ext.flatten
|
import org.koitharu.kotatsu.core.util.ext.flatten
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
|
||||||
import org.koitharu.kotatsu.core.util.ext.takeMostFrequent
|
import org.koitharu.kotatsu.core.util.ext.takeMostFrequent
|
||||||
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
|
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
|
||||||
import org.koitharu.kotatsu.core.util.ext.trySetForeground
|
import org.koitharu.kotatsu.core.util.ext.trySetForeground
|
||||||
@@ -289,7 +290,7 @@ class SuggestionsWorker @AssistedInject constructor(
|
|||||||
style.bigText(
|
style.bigText(
|
||||||
buildSpannedString {
|
buildSpannedString {
|
||||||
append(tagsText)
|
append(tagsText)
|
||||||
val chaptersCount = manga.chapters?.size ?: 0
|
val chaptersCount = manga.chapters.sizeOrZero()
|
||||||
appendLine()
|
appendLine()
|
||||||
bold {
|
bold {
|
||||||
append(
|
append(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.model.getPreferredBranch
|
|||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.CompositeMutex2
|
import org.koitharu.kotatsu.core.util.MultiMutex
|
||||||
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
|
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
@@ -139,7 +139,7 @@ class Tracker @Inject constructor(
|
|||||||
private companion object {
|
private companion object {
|
||||||
|
|
||||||
const val NO_ID = 0L
|
const val NO_ID = 0L
|
||||||
private val mangaMutex = CompositeMutex2<Long>()
|
private val mangaMutex = MultiMutex<Long>()
|
||||||
|
|
||||||
suspend inline fun <T> withMangaLock(id: Long, action: () -> T): T {
|
suspend inline fun <T> withMangaLock(id: Long, action: () -> T): T {
|
||||||
contract {
|
contract {
|
||||||
|
|||||||
@@ -636,4 +636,7 @@
|
|||||||
<string name="new_chapters_pattern">%1$s: %2$d</string>
|
<string name="new_chapters_pattern">%1$s: %2$d</string>
|
||||||
<string name="pin_navigation_ui">Pin navigation UI</string>
|
<string name="pin_navigation_ui">Pin navigation UI</string>
|
||||||
<string name="pin_navigation_ui_summary">Do not hide navgation bar and search view on scroll</string>
|
<string name="pin_navigation_ui_summary">Do not hide navgation bar and search view on scroll</string>
|
||||||
|
<string name="search_suggestions">Search suggestions</string>
|
||||||
|
<string name="recent_queries">Recent queries</string>
|
||||||
|
<string name="suggested_queries">Suggested queries</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
android:summary="@string/history_shortcuts_summary"
|
android:summary="@string/history_shortcuts_summary"
|
||||||
android:title="@string/history_shortcuts" />
|
android:title="@string/history_shortcuts" />
|
||||||
|
|
||||||
|
<MultiSelectListPreference
|
||||||
|
android:key="search_suggest_types"
|
||||||
|
android:title="@string/search_suggestions" />
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/backup_restore">
|
<PreferenceCategory android:title="@string/backup_restore">
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ import kotlinx.coroutines.withTimeoutOrNull
|
|||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.koitharu.kotatsu.core.util.CompositeMutex
|
import org.koitharu.kotatsu.core.util.MultiMutex
|
||||||
|
|
||||||
class CompositeMutexTest {
|
class MultiMutexTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun singleLock() = runTest {
|
fun singleLock() = runTest {
|
||||||
val mutex = CompositeMutex<Int>()
|
val mutex = MultiMutex<Int>()
|
||||||
mutex.lock(1)
|
mutex.lock(1)
|
||||||
mutex.lock(2)
|
mutex.lock(2)
|
||||||
mutex.unlock(1)
|
mutex.unlock(1)
|
||||||
@@ -26,8 +27,9 @@ class CompositeMutexTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Cannot delay in test")
|
||||||
fun doubleLock() = runTest {
|
fun doubleLock() = runTest {
|
||||||
val mutex = CompositeMutex<Int>()
|
val mutex = MultiMutex<Int>()
|
||||||
repeat(2) {
|
repeat(2) {
|
||||||
launch(Dispatchers.Default) {
|
launch(Dispatchers.Default) {
|
||||||
mutex.lock(1)
|
mutex.lock(1)
|
||||||
@@ -44,7 +46,7 @@ class CompositeMutexTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun cancellation() = runTest {
|
fun cancellation() = runTest {
|
||||||
val mutex = CompositeMutex<Int>()
|
val mutex = MultiMutex<Int>()
|
||||||
mutex.lock(1)
|
mutex.lock(1)
|
||||||
val job = launch {
|
val job = launch {
|
||||||
try {
|
try {
|
||||||
Reference in New Issue
Block a user