Improve global search
This commit is contained in:
@@ -19,8 +19,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1002
|
versionCode = 1003
|
||||||
versionName = '8.0-b2'
|
versionName = '8.0-b3'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||||
ksp {
|
ksp {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -135,3 +136,9 @@ suspend fun <T : Any> Flow<T?>.firstNotNullOrNull(): T? = firstOrNull { x -> x !
|
|||||||
fun <T> Flow<Flow<T>>.flattenLatest() = flatMapLatest { it }
|
fun <T> Flow<Flow<T>>.flattenLatest() = flatMapLatest { it }
|
||||||
|
|
||||||
fun <T> SuspendLazy<T>.asFlow() = flow { emit(runCatchingCancellable { get() }) }
|
fun <T> SuspendLazy<T>.asFlow() = flow { emit(runCatchingCancellable { get() }) }
|
||||||
|
|
||||||
|
suspend fun <T> SendChannel<T>.sendNotNull(item: T?) {
|
||||||
|
if (item != null) {
|
||||||
|
send(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package org.koitharu.kotatsu.search.domain
|
|||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import org.koitharu.kotatsu.core.model.isNsfw
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
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.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
@@ -20,9 +22,13 @@ class SearchV2Helper @AssistedInject constructor(
|
|||||||
@Assisted private val source: MangaSource,
|
@Assisted private val source: MangaSource,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val dataRepository: MangaDataRepository,
|
private val dataRepository: MangaDataRepository,
|
||||||
|
private val settings: AppSettings,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(query: String, kind: SearchKind): SearchResults? {
|
suspend operator fun invoke(query: String, kind: SearchKind): SearchResults? {
|
||||||
|
if (settings.isNsfwContentDisabled && source.isNsfw()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
val repository = mangaRepositoryFactory.create(source)
|
val repository = mangaRepositoryFactory.create(source)
|
||||||
val listFilter = repository.getFilter(query, kind) ?: return null
|
val listFilter = repository.getFilter(query, kind) ?: return null
|
||||||
val sortOrder = repository.getSortOrder(kind)
|
val sortOrder = repository.getSortOrder(kind)
|
||||||
@@ -68,6 +74,9 @@ class SearchV2Helper @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<Manga>.postFilter(query: String, kind: SearchKind) {
|
private fun MutableList<Manga>.postFilter(query: String, kind: SearchKind) {
|
||||||
|
if (settings.isNsfwContentDisabled) {
|
||||||
|
removeAll { it.isNsfw }
|
||||||
|
}
|
||||||
when (kind) {
|
when (kind) {
|
||||||
SearchKind.TITLE -> retainAll { m ->
|
SearchKind.TITLE -> retainAll { m ->
|
||||||
m.matches(query, MATCH_THRESHOLD_DEFAULT)
|
m.matches(query, MATCH_THRESHOLD_DEFAULT)
|
||||||
|
|||||||
@@ -140,10 +140,12 @@ class SearchActivity :
|
|||||||
|
|
||||||
override fun onFilterClick(view: View?) = Unit
|
override fun onFilterClick(view: View?) = Unit
|
||||||
|
|
||||||
override fun onEmptyActionClick() = Unit
|
override fun onEmptyActionClick() = viewModel.continueSearch()
|
||||||
|
|
||||||
override fun onListHeaderClick(item: ListHeader, view: View) = Unit
|
override fun onListHeaderClick(item: ListHeader, view: View) = Unit
|
||||||
|
|
||||||
|
override fun onFooterButtonClick() = viewModel.continueSearch()
|
||||||
|
|
||||||
override fun onPrimaryButtonClick(tipView: TipView) = Unit
|
override fun onPrimaryButtonClick(tipView: TipView) = Unit
|
||||||
|
|
||||||
override fun onSecondaryButtonClick(tipView: TipView) = Unit
|
override fun onSecondaryButtonClick(tipView: TipView) = Unit
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
package org.koitharu.kotatsu.search.ui.multi
|
package org.koitharu.kotatsu.search.ui.multi
|
||||||
|
|
||||||
import androidx.annotation.CheckResult
|
import androidx.collection.ArraySet
|
||||||
import androidx.collection.LongSet
|
import androidx.collection.LongSet
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.onEmpty
|
|
||||||
import kotlinx.coroutines.flow.runningFold
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
@@ -29,18 +26,23 @@ import org.koitharu.kotatsu.core.nav.AppRouter
|
|||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||||
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
|
||||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ButtonFooter
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.search.domain.SearchKind
|
import org.koitharu.kotatsu.search.domain.SearchKind
|
||||||
import org.koitharu.kotatsu.search.domain.SearchV2Helper
|
import org.koitharu.kotatsu.search.domain.SearchV2Helper
|
||||||
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val MAX_PARALLELISM = 4
|
private const val MAX_PARALLELISM = 4
|
||||||
@@ -58,15 +60,16 @@ class SearchViewModel @Inject constructor(
|
|||||||
val query = savedStateHandle.get<String>(AppRouter.KEY_QUERY).orEmpty()
|
val query = savedStateHandle.get<String>(AppRouter.KEY_QUERY).orEmpty()
|
||||||
val kind = savedStateHandle.get<SearchKind>(AppRouter.KEY_KIND) ?: SearchKind.SIMPLE
|
val kind = savedStateHandle.get<SearchKind>(AppRouter.KEY_KIND) ?: SearchKind.SIMPLE
|
||||||
|
|
||||||
private val retryCounter = MutableStateFlow(0)
|
private var includeDisabledSources = MutableStateFlow(false)
|
||||||
private val listData = retryCounter.flatMapLatest {
|
private val results = MutableStateFlow<List<SearchResultsListModel>>(emptyList())
|
||||||
searchImpl().withLoading().withErrorHandling()
|
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
private var searchJob: Job? = null
|
||||||
|
|
||||||
val list: StateFlow<List<ListModel>> = combine(
|
val list: StateFlow<List<ListModel>> = combine(
|
||||||
listData.filterNotNull(),
|
results,
|
||||||
isLoading,
|
isLoading,
|
||||||
) { list, loading ->
|
includeDisabledSources,
|
||||||
|
) { list, loading, includeDisabled ->
|
||||||
when {
|
when {
|
||||||
list.isEmpty() -> listOf(
|
list.isEmpty() -> listOf(
|
||||||
when {
|
when {
|
||||||
@@ -81,13 +84,18 @@ class SearchViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
loading -> list + LoadingFooter()
|
loading -> list + LoadingFooter()
|
||||||
else -> list
|
includeDisabled -> list
|
||||||
|
else -> list + ButtonFooter(R.string.search_disabled_sources)
|
||||||
}
|
}
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||||
|
|
||||||
|
init {
|
||||||
|
doSearch()
|
||||||
|
}
|
||||||
|
|
||||||
fun getItems(ids: LongSet): Set<Manga> {
|
fun getItems(ids: LongSet): Set<Manga> {
|
||||||
val snapshot = listData.value ?: return emptySet()
|
val snapshot = results.value
|
||||||
val result = HashSet<Manga>(ids.size)
|
val result = ArraySet<Manga>(ids.size)
|
||||||
snapshot.forEach { x ->
|
snapshot.forEach { x ->
|
||||||
for (item in x.list) {
|
for (item in x.list) {
|
||||||
if (item.id in ids) {
|
if (item.id in ids) {
|
||||||
@@ -99,157 +107,192 @@ class SearchViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun retry() {
|
fun retry() {
|
||||||
retryCounter.value += 1
|
searchJob?.cancel()
|
||||||
|
results.value = emptyList()
|
||||||
|
includeDisabledSources.value = false
|
||||||
|
doSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckResult
|
fun continueSearch() {
|
||||||
private fun searchImpl(): Flow<List<SearchResultsListModel>> = channelFlow {
|
if (includeDisabledSources.value) {
|
||||||
searchHistory()?.let { send(it) }
|
return
|
||||||
searchFavorites()?.let { send(it) }
|
|
||||||
searchLocal()?.let { send(it) }
|
|
||||||
val sources = sourcesRepository.getEnabledSources()
|
|
||||||
if (sources.isEmpty()) {
|
|
||||||
return@channelFlow
|
|
||||||
}
|
}
|
||||||
val semaphore = Semaphore(MAX_PARALLELISM)
|
val prevJob = searchJob
|
||||||
sources.map { source ->
|
searchJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
launch {
|
includeDisabledSources.value = true
|
||||||
val item = runCatchingCancellable {
|
prevJob?.join()
|
||||||
|
val sources = sourcesRepository.getDisabledSources()
|
||||||
|
.sortedByDescending { it.priority() }
|
||||||
|
val semaphore = Semaphore(MAX_PARALLELISM)
|
||||||
|
sources.map { source ->
|
||||||
|
launch {
|
||||||
semaphore.withPermit {
|
semaphore.withPermit {
|
||||||
val searchHelper = searchHelperFactory.create(source)
|
appendResult(searchSource(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,
|
|
||||||
list = list,
|
|
||||||
error = null,
|
|
||||||
listFilter = result.listFilter,
|
|
||||||
sortOrder = result.sortOrder,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFailure = { error ->
|
|
||||||
error.printStackTraceDebug()
|
|
||||||
SearchResultsListModel(0, source, null, null, emptyList(), error)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (item != null) {
|
|
||||||
send(item)
|
|
||||||
}
|
}
|
||||||
}
|
}.joinAll()
|
||||||
}.joinAll()
|
}
|
||||||
}.runningFold<SearchResultsListModel, List<SearchResultsListModel>?>(null) { list, item -> list.orEmpty() + item }
|
}
|
||||||
.filterNotNull()
|
|
||||||
.onEmpty { emit(emptyList()) }
|
|
||||||
|
|
||||||
private suspend fun searchHistory(): SearchResultsListModel? {
|
private fun doSearch() {
|
||||||
return runCatchingCancellable {
|
val prevJob = searchJob
|
||||||
historyRepository.search(query, kind, Int.MAX_VALUE)
|
searchJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
}.fold(
|
prevJob?.cancelAndJoin()
|
||||||
onSuccess = { result ->
|
appendResult(searchHistory())
|
||||||
if (result.isNotEmpty()) {
|
appendResult(searchFavorites())
|
||||||
SearchResultsListModel(
|
appendResult(searchLocal())
|
||||||
titleResId = R.string.history,
|
val sources = sourcesRepository.getEnabledSources()
|
||||||
source = UnknownMangaSource,
|
val semaphore = Semaphore(MAX_PARALLELISM)
|
||||||
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
|
sources.map { source ->
|
||||||
error = null,
|
launch {
|
||||||
listFilter = null,
|
semaphore.withPermit {
|
||||||
sortOrder = null,
|
appendResult(searchSource(source))
|
||||||
)
|
}
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
},
|
}.joinAll()
|
||||||
onFailure = { error ->
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl
|
||||||
|
|
||||||
|
private suspend fun searchSource(source: MangaSource): SearchResultsListModel? = runCatchingCancellable {
|
||||||
|
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,
|
||||||
|
list = list,
|
||||||
|
error = null,
|
||||||
|
listFilter = result.listFilter,
|
||||||
|
sortOrder = result.sortOrder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
error.printStackTraceDebug()
|
||||||
|
if (source is MangaParserSource && source.isBroken) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
SearchResultsListModel(0, source, null, null, emptyList(), error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun searchHistory(): SearchResultsListModel? = runCatchingCancellable {
|
||||||
|
historyRepository.search(query, kind, Int.MAX_VALUE)
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { result ->
|
||||||
|
if (result.isNotEmpty()) {
|
||||||
SearchResultsListModel(
|
SearchResultsListModel(
|
||||||
titleResId = R.string.history,
|
titleResId = R.string.history,
|
||||||
source = UnknownMangaSource,
|
source = UnknownMangaSource,
|
||||||
list = emptyList(),
|
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
|
||||||
error = error,
|
error = null,
|
||||||
listFilter = null,
|
listFilter = null,
|
||||||
sortOrder = null,
|
sortOrder = null,
|
||||||
)
|
)
|
||||||
},
|
} else {
|
||||||
)
|
null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
SearchResultsListModel(
|
||||||
|
titleResId = R.string.history,
|
||||||
|
source = UnknownMangaSource,
|
||||||
|
list = emptyList(),
|
||||||
|
error = error,
|
||||||
|
listFilter = null,
|
||||||
|
sortOrder = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
private suspend fun searchFavorites(): SearchResultsListModel? {
|
private suspend fun searchFavorites(): SearchResultsListModel? = runCatchingCancellable {
|
||||||
return runCatchingCancellable {
|
favouritesRepository.search(query, kind, Int.MAX_VALUE)
|
||||||
favouritesRepository.search(query, kind, Int.MAX_VALUE)
|
}.fold(
|
||||||
}.fold(
|
onSuccess = { result ->
|
||||||
onSuccess = { result ->
|
if (result.isNotEmpty()) {
|
||||||
if (result.isNotEmpty()) {
|
|
||||||
SearchResultsListModel(
|
|
||||||
titleResId = R.string.favourites,
|
|
||||||
source = UnknownMangaSource,
|
|
||||||
list = mangaListMapper.toListModelList(
|
|
||||||
manga = result,
|
|
||||||
mode = ListMode.GRID,
|
|
||||||
flags = MangaListMapper.NO_FAVORITE,
|
|
||||||
),
|
|
||||||
error = null,
|
|
||||||
listFilter = null,
|
|
||||||
sortOrder = null,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFailure = { error ->
|
|
||||||
SearchResultsListModel(
|
SearchResultsListModel(
|
||||||
titleResId = R.string.favourites,
|
titleResId = R.string.favourites,
|
||||||
source = UnknownMangaSource,
|
source = UnknownMangaSource,
|
||||||
list = emptyList(),
|
list = mangaListMapper.toListModelList(
|
||||||
error = error,
|
manga = result,
|
||||||
|
mode = ListMode.GRID,
|
||||||
|
flags = MangaListMapper.NO_FAVORITE,
|
||||||
|
),
|
||||||
|
error = null,
|
||||||
listFilter = null,
|
listFilter = null,
|
||||||
sortOrder = null,
|
sortOrder = null,
|
||||||
)
|
)
|
||||||
},
|
} else {
|
||||||
)
|
null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
SearchResultsListModel(
|
||||||
|
titleResId = R.string.favourites,
|
||||||
|
source = UnknownMangaSource,
|
||||||
|
list = emptyList(),
|
||||||
|
error = error,
|
||||||
|
listFilter = null,
|
||||||
|
sortOrder = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
private suspend fun searchLocal(): SearchResultsListModel? {
|
private suspend fun searchLocal(): SearchResultsListModel? = runCatchingCancellable {
|
||||||
return runCatchingCancellable {
|
searchHelperFactory.create(LocalMangaSource).invoke(query, kind)
|
||||||
searchHelperFactory.create(LocalMangaSource).invoke(query, kind)
|
}.fold(
|
||||||
}.fold(
|
onSuccess = { result ->
|
||||||
onSuccess = { result ->
|
if (!result?.manga.isNullOrEmpty()) {
|
||||||
if (!result?.manga.isNullOrEmpty()) {
|
|
||||||
SearchResultsListModel(
|
|
||||||
titleResId = 0,
|
|
||||||
source = LocalMangaSource,
|
|
||||||
list = mangaListMapper.toListModelList(
|
|
||||||
manga = result.manga,
|
|
||||||
mode = ListMode.GRID,
|
|
||||||
flags = MangaListMapper.NO_SAVED,
|
|
||||||
),
|
|
||||||
error = null,
|
|
||||||
listFilter = result.listFilter,
|
|
||||||
sortOrder = result.sortOrder,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFailure = { error ->
|
|
||||||
SearchResultsListModel(
|
SearchResultsListModel(
|
||||||
titleResId = 0,
|
titleResId = 0,
|
||||||
source = LocalMangaSource,
|
source = LocalMangaSource,
|
||||||
list = emptyList(),
|
list = mangaListMapper.toListModelList(
|
||||||
error = error,
|
manga = result.manga,
|
||||||
listFilter = null,
|
mode = ListMode.GRID,
|
||||||
sortOrder = null,
|
flags = MangaListMapper.NO_SAVED,
|
||||||
|
),
|
||||||
|
error = null,
|
||||||
|
listFilter = result.listFilter,
|
||||||
|
sortOrder = result.sortOrder,
|
||||||
)
|
)
|
||||||
},
|
} else {
|
||||||
)
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
SearchResultsListModel(
|
||||||
|
titleResId = 0,
|
||||||
|
source = LocalMangaSource,
|
||||||
|
list = emptyList(),
|
||||||
|
error = error,
|
||||||
|
listFilter = null,
|
||||||
|
sortOrder = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun appendResult(item: SearchResultsListModel?) {
|
||||||
|
if (item != null) {
|
||||||
|
results.update { list -> list + item }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MangaSource.priority(): Int {
|
||||||
|
var res = 0
|
||||||
|
if (this is MangaParserSource) {
|
||||||
|
if (locale.toLocale() == Locale.getDefault()) res += 2
|
||||||
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
|||||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.buttonFooterAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
@@ -45,6 +46,7 @@ class SearchAdapter(
|
|||||||
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
|
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
|
||||||
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
|
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
|
||||||
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener))
|
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener))
|
||||||
|
addDelegate(ListItemType.FOOTER_BUTTON, buttonFooterAD(listener))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import okio.FileNotFoundException
|
||||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
@@ -67,7 +68,7 @@ class BackupViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun saveBackup(output: Uri) {
|
fun saveBackup(output: Uri) {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
val file = checkNotNull(backupFile)
|
val file = backupFile ?: throw FileNotFoundException()
|
||||||
contentResolver.openFileDescriptor(output, "w")?.use { fd ->
|
contentResolver.openFileDescriptor(output, "w")?.use { fd ->
|
||||||
FileOutputStream(fd.fileDescriptor).use {
|
FileOutputStream(fd.fileDescriptor).use {
|
||||||
it.write(file.readBytes())
|
it.write(file.readBytes())
|
||||||
|
|||||||
@@ -809,4 +809,5 @@
|
|||||||
<string name="chapter_volume_number">Vol %1$s Chapter %2$s</string>
|
<string name="chapter_volume_number">Vol %1$s Chapter %2$s</string>
|
||||||
<string name="chapter_number">Chapter %s</string>
|
<string name="chapter_number">Chapter %s</string>
|
||||||
<string name="unnamed_chapter">Unnamed chapter</string>
|
<string name="unnamed_chapter">Unnamed chapter</string>
|
||||||
|
<string name="search_disabled_sources">Search through disabled sources</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ material = "1.13.0-alpha11"
|
|||||||
moshi = "1.15.2"
|
moshi = "1.15.2"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
okio = "3.10.2"
|
okio = "3.10.2"
|
||||||
parsers = "531145c7f9"
|
parsers = "77a5216ebf"
|
||||||
preference = "1.2.1"
|
preference = "1.2.1"
|
||||||
recyclerview = "1.4.0"
|
recyclerview = "1.4.0"
|
||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user