Various fixes

This commit is contained in:
Koitharu
2024-09-24 17:47:35 +03:00
committed by Mac135135
parent 8c0617c525
commit 5a75fe77fd
9 changed files with 78 additions and 74 deletions

View File

@@ -65,6 +65,7 @@ abstract class BaseViewModel : ViewModel() {
}
protected fun <T> Flow<T>.withErrorHandling() = catch { error ->
error.printStackTraceDebug()
errorEvent.call(error)
}

View File

@@ -97,3 +97,14 @@ fun LongSet.toSet(): Set<Long> = toCollection(ArraySet<Long>(size))
fun <R : MutableCollection<Long>> LongSet.toCollection(out: R): R = out.also { result ->
forEach(result::add)
}
fun <T, R> Collection<T>.mapSortedByCount(isDescending: Boolean = true, mapper: (T) -> R): List<R> {
val grouped = groupBy(mapper).toList()
val sortSelector: (Pair<R, List<T>>) -> Int = { it.second.size }
val sorted = if (isDescending) {
grouped.sortedByDescending(sortSelector)
} else {
grouped.sortedBy(sortSelector)
}
return sorted.map { it.first }
}

View File

@@ -13,7 +13,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.BuildConfig
@@ -50,14 +49,13 @@ class MangaSourcesRepository @Inject constructor(
private val dao: MangaSourcesDao
get() = db.getSourcesDao()
private val remoteSources = EnumSet.allOf(MangaParserSource::class.java).apply {
if (!BuildConfig.DEBUG) {
remove(MangaParserSource.DUMMY)
}
}
val allMangaSources: Set<MangaParserSource>
get() = Collections.unmodifiableSet(remoteSources)
val allMangaSources: Set<MangaParserSource> = Collections.unmodifiableSet(
EnumSet.allOf(MangaParserSource::class.java).apply {
if (!BuildConfig.DEBUG) {
remove(MangaParserSource.DUMMY)
}
},
)
suspend fun getEnabledSources(): List<MangaSource> {
assimilateNewSources()
@@ -86,7 +84,7 @@ class MangaSourcesRepository @Inject constructor(
suspend fun getDisabledSources(): Set<MangaSource> {
assimilateNewSources()
val result = EnumSet.copyOf(remoteSources)
val result = EnumSet.copyOf(allMangaSources)
val enabled = dao.findAllEnabledNames()
for (name in enabled) {
val source = name.toMangaSourceOrNull() ?: continue
@@ -182,7 +180,7 @@ class MangaSourcesRepository @Inject constructor(
val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size)
for (entity in entities) {
val source = entity.source.toMangaSourceOrNull() ?: continue
if (source in remoteSources) {
if (source in allMangaSources) {
result.add(source to entity.isEnabled)
}
}
@@ -199,7 +197,7 @@ class MangaSourcesRepository @Inject constructor(
suspend fun setSourcesEnabledExclusive(sources: Set<MangaSource>) {
db.withTransaction {
assimilateNewSources()
for (s in remoteSources) {
for (s in allMangaSources) {
dao.setEnabled(s.name, s in sources)
}
}
@@ -222,7 +220,7 @@ class MangaSourcesRepository @Inject constructor(
fun observeHasNewSources(): Flow<Boolean> = observeIsNsfwDisabled().map { skipNsfw ->
val sources = dao.findAllFromVersion(BuildConfig.VERSION_CODE).toSources(skipNsfw, null)
sources.isNotEmpty() && sources.size != remoteSources.size
sources.isNotEmpty() && sources.size != allMangaSources.size
}.onStart { assimilateNewSources() }
fun observeHasNewSourcesForBadge(): Flow<Boolean> = combine(
@@ -295,7 +293,7 @@ class MangaSourcesRepository @Inject constructor(
private suspend fun getNewSources(): MutableSet<out MangaSource> {
val entities = dao.findAll()
val result = EnumSet.copyOf(remoteSources)
val result = EnumSet.copyOf(allMangaSources)
for (e in entities) {
result.remove(e.source.toMangaSourceOrNull() ?: continue)
}
@@ -361,7 +359,7 @@ class MangaSourcesRepository @Inject constructor(
if (skipNsfwSources && source.isNsfw()) {
continue
}
if (source in remoteSources) {
if (source in allMangaSources) {
result.add(
MangaSourceInfo(
mangaSource = source,

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.favourites.domain
import dagger.Reusable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapLatest
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
@@ -18,24 +17,20 @@ import javax.inject.Inject
class LocalFavoritesObserver @Inject constructor(
localMangaIndex: LocalMangaIndex,
private val db: MangaDatabase,
) : LocalObserveMapper<FavouriteManga, Manga>(localMangaIndex, limitStep = 10) {
) : LocalObserveMapper<FavouriteManga, Manga>(localMangaIndex) {
fun observeAll(
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
): Flow<List<Manga>> = db.getFavouritesDao().observeAll(order, filterOptions, limit).mapLatest {
it.mapToLocal()
}
): Flow<List<Manga>> = db.getFavouritesDao().observeAll(order, filterOptions, limit).mapToLocal()
fun observeAll(
categoryId: Long,
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
): Flow<List<Manga>> = db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit).mapLatest {
it.mapToLocal()
}
): Flow<List<Manga>> = db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit).mapToLocal()
override fun toManga(e: FavouriteManga) = e.manga.toManga(e.tags.toMangaTags())

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.history.data
import dagger.Reusable
import kotlinx.coroutines.flow.mapLatest
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
@@ -17,15 +16,13 @@ import javax.inject.Inject
class HistoryLocalObserver @Inject constructor(
localMangaIndex: LocalMangaIndex,
private val db: MangaDatabase,
) : LocalObserveMapper<HistoryWithManga, MangaWithHistory>(localMangaIndex, limitStep = 10) {
) : LocalObserveMapper<HistoryWithManga, MangaWithHistory>(localMangaIndex) {
fun observeAll(
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
) = db.getHistoryDao().observeAll(order, filterOptions, limit).mapLatest {
it.mapToLocal()
}
) = db.getHistoryDao().observeAll(order, filterOptions, limit).mapToLocal()
override fun toManga(e: HistoryWithManga) = e.manga.toManga(e.tags.toMangaTags())

View File

@@ -4,19 +4,15 @@ import android.content.Context
import androidx.core.content.edit
import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File
import javax.inject.Inject
@@ -27,7 +23,6 @@ import javax.inject.Singleton
class LocalMangaIndex @Inject constructor(
private val mangaDataRepository: MangaDataRepository,
private val db: MangaDatabase,
private val localStorageManager: LocalStorageManager,
@ApplicationContext context: Context,
private val localMangaRepositoryProvider: Provider<LocalMangaRepository>,
) : FlowCollector<LocalManga?> {
@@ -35,9 +30,9 @@ class LocalMangaIndex @Inject constructor(
private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
private val mutex = Mutex()
private var previousHash: Long
get() = prefs.getLong(KEY_HASH, 0L)
set(value) = prefs.edit { putLong(KEY_HASH, value) }
private var currentVersion: Int
get() = prefs.getInt(KEY_VERSION, 0)
set(value) = prefs.edit { putInt(KEY_VERSION, value) }
override suspend fun emit(value: LocalManga?) {
if (value != null) {
@@ -45,22 +40,25 @@ class LocalMangaIndex @Inject constructor(
}
}
suspend fun update(): Boolean = mutex.withLock {
val newHash = computeHash()
if (newHash == previousHash) {
return false
}
suspend fun update() = mutex.withLock {
db.withTransaction {
val dao = db.getLocalMangaIndexDao()
dao.clear()
localMangaRepositoryProvider.get().getRawListAsFlow()
.collect { dao.upsert(it.toEntity()) }
localMangaRepositoryProvider.get()
.getRawListAsFlow()
.collect { upsert(it) }
}
currentVersion = VERSION
}
suspend fun updateIfRequired() {
if (isUpdateRequired()) {
update()
}
previousHash = newHash
return true
}
suspend fun get(mangaId: Long): LocalManga? {
updateIfRequired()
var path = db.getLocalMangaIndexDao().findPath(mangaId)
if (path == null && mutex.isLocked) { // wait for updating complete
path = mutex.withLock { db.getLocalMangaIndexDao().findPath(mangaId) }
@@ -77,8 +75,7 @@ class LocalMangaIndex @Inject constructor(
suspend fun put(manga: LocalManga) = mutex.withLock {
db.withTransaction {
mangaDataRepository.storeManga(manga.manga)
db.getLocalMangaIndexDao().upsert(manga.toEntity())
upsert(manga)
}
}
@@ -90,27 +87,22 @@ class LocalMangaIndex @Inject constructor(
return db.getLocalMangaIndexDao().findTags()
}
private suspend fun upsert(manga: LocalManga) {
mangaDataRepository.storeManga(manga.manga)
db.getLocalMangaIndexDao().upsert(manga.toEntity())
}
private fun LocalManga.toEntity() = LocalMangaIndexEntity(
mangaId = manga.id,
path = file.path,
)
private suspend fun computeHash(): Long {
return runCatchingCancellable {
localStorageManager.getReadableDirs()
.fold(0L) { acc, file -> acc + file.computeHash() }
}.onFailure {
it.printStackTraceDebug()
}.getOrDefault(0L)
}
private suspend fun File.computeHash(): Long = runInterruptible(Dispatchers.IO) {
lastModified() // TODO size
}
private fun isUpdateRequired() = currentVersion < VERSION
companion object {
private const val PREF_NAME = "_local_index"
private const val KEY_HASH = "hash"
private const val KEY_VERSION = "ver"
private const val VERSION = 1
}
}

View File

@@ -4,16 +4,24 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.parsers.model.Manga
abstract class LocalObserveMapper<E : Any, R : Any>(
private val localMangaIndex: LocalMangaIndex,
private val limitStep: Int,
) {
protected suspend fun List<E>.mapToLocal(): List<R> = coroutineScope {
protected fun Flow<Collection<E>>.mapToLocal() = onStart {
localMangaIndex.updateIfRequired()
}.mapLatest {
it.mapToLocal()
}
private suspend fun Collection<E>.mapToLocal(): List<R> = coroutineScope {
val dispatcher = Dispatchers.IO.limitedParallelism(6)
map { item ->
val m = toManga(item)

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.mapSortedByCount
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.core.util.ext.toLocale
@@ -43,15 +44,20 @@ class WelcomeViewModel @Inject constructor(
val types = MutableStateFlow(
FilterProperty(
availableItems = ContentType.entries.toList(),
availableItems = listOf(ContentType.MANGA),
selectedItems = setOf(ContentType.MANGA),
isLoading = false,
isLoading = true,
error = null,
),
)
init {
updateJob = launchJob(Dispatchers.Default) {
val contentTypes = allSources.mapSortedByCount { it.contentType }
types.value = types.value.copy(
availableItems = contentTypes,
isLoading = false,
)
val languages = localesGroups.keys.associateBy { x -> x.language }
val selectedLocales = HashSet<Locale>(2)
ConfigurationCompat.getLocales(context.resources.configuration).toList()

View File

@@ -14,20 +14,18 @@ import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.mapSortedByCount
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.EnumMap
import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject
@@ -139,13 +137,11 @@ class SourcesCatalogViewModel @Inject constructor(
@WorkerThread
private fun getContentTypes(isNsfwDisabled: Boolean): List<ContentType> {
val map = EnumMap<ContentType, Int>(ContentType::class.java)
for (e in MangaParserSource.entries) {
if (isNsfwDisabled && e.isNsfw()) {
continue
}
map[e.contentType] = map.getOrDefault(e.contentType, 0) + 1
val result = repository.allMangaSources.mapSortedByCount { it.contentType }
return if (isNsfwDisabled) {
result.filterNot { it == ContentType.HENTAI }
} else {
result
}
return map.entries.sortedByDescending { it.value }.map { it.key }
}
}