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

View File

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

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.history.data package org.koitharu.kotatsu.history.data
import dagger.Reusable import dagger.Reusable
import kotlinx.coroutines.flow.mapLatest
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.db.entity.toMangaTags
@@ -17,15 +16,13 @@ import javax.inject.Inject
class HistoryLocalObserver @Inject constructor( class HistoryLocalObserver @Inject constructor(
localMangaIndex: LocalMangaIndex, localMangaIndex: LocalMangaIndex,
private val db: MangaDatabase, private val db: MangaDatabase,
) : LocalObserveMapper<HistoryWithManga, MangaWithHistory>(localMangaIndex, limitStep = 10) { ) : LocalObserveMapper<HistoryWithManga, MangaWithHistory>(localMangaIndex) {
fun observeAll( fun observeAll(
order: ListSortOrder, order: ListSortOrder,
filterOptions: Set<ListFilterOption>, filterOptions: Set<ListFilterOption>,
limit: Int limit: Int
) = db.getHistoryDao().observeAll(order, filterOptions, limit).mapLatest { ) = db.getHistoryDao().observeAll(order, filterOptions, limit).mapToLocal()
it.mapToLocal()
}
override fun toManga(e: HistoryWithManga) = e.manga.toManga(e.tags.toMangaTags()) 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.core.content.edit
import androidx.room.withTransaction import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.local.data.LocalMangaRepository 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.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@@ -27,7 +23,6 @@ import javax.inject.Singleton
class LocalMangaIndex @Inject constructor( class LocalMangaIndex @Inject constructor(
private val mangaDataRepository: MangaDataRepository, private val mangaDataRepository: MangaDataRepository,
private val db: MangaDatabase, private val db: MangaDatabase,
private val localStorageManager: LocalStorageManager,
@ApplicationContext context: Context, @ApplicationContext context: Context,
private val localMangaRepositoryProvider: Provider<LocalMangaRepository>, private val localMangaRepositoryProvider: Provider<LocalMangaRepository>,
) : FlowCollector<LocalManga?> { ) : FlowCollector<LocalManga?> {
@@ -35,9 +30,9 @@ class LocalMangaIndex @Inject constructor(
private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
private val mutex = Mutex() private val mutex = Mutex()
private var previousHash: Long private var currentVersion: Int
get() = prefs.getLong(KEY_HASH, 0L) get() = prefs.getInt(KEY_VERSION, 0)
set(value) = prefs.edit { putLong(KEY_HASH, value) } set(value) = prefs.edit { putInt(KEY_VERSION, value) }
override suspend fun emit(value: LocalManga?) { override suspend fun emit(value: LocalManga?) {
if (value != null) { if (value != null) {
@@ -45,22 +40,25 @@ class LocalMangaIndex @Inject constructor(
} }
} }
suspend fun update(): Boolean = mutex.withLock { suspend fun update() = mutex.withLock {
val newHash = computeHash()
if (newHash == previousHash) {
return false
}
db.withTransaction { db.withTransaction {
val dao = db.getLocalMangaIndexDao() val dao = db.getLocalMangaIndexDao()
dao.clear() dao.clear()
localMangaRepositoryProvider.get().getRawListAsFlow() localMangaRepositoryProvider.get()
.collect { dao.upsert(it.toEntity()) } .getRawListAsFlow()
.collect { upsert(it) }
}
currentVersion = VERSION
}
suspend fun updateIfRequired() {
if (isUpdateRequired()) {
update()
} }
previousHash = newHash
return true
} }
suspend fun get(mangaId: Long): LocalManga? { suspend fun get(mangaId: Long): LocalManga? {
updateIfRequired()
var path = db.getLocalMangaIndexDao().findPath(mangaId) var path = db.getLocalMangaIndexDao().findPath(mangaId)
if (path == null && mutex.isLocked) { // wait for updating complete if (path == null && mutex.isLocked) { // wait for updating complete
path = mutex.withLock { db.getLocalMangaIndexDao().findPath(mangaId) } path = mutex.withLock { db.getLocalMangaIndexDao().findPath(mangaId) }
@@ -77,8 +75,7 @@ class LocalMangaIndex @Inject constructor(
suspend fun put(manga: LocalManga) = mutex.withLock { suspend fun put(manga: LocalManga) = mutex.withLock {
db.withTransaction { db.withTransaction {
mangaDataRepository.storeManga(manga.manga) upsert(manga)
db.getLocalMangaIndexDao().upsert(manga.toEntity())
} }
} }
@@ -90,27 +87,22 @@ class LocalMangaIndex @Inject constructor(
return db.getLocalMangaIndexDao().findTags() return db.getLocalMangaIndexDao().findTags()
} }
private suspend fun upsert(manga: LocalManga) {
mangaDataRepository.storeManga(manga.manga)
db.getLocalMangaIndexDao().upsert(manga.toEntity())
}
private fun LocalManga.toEntity() = LocalMangaIndexEntity( private fun LocalManga.toEntity() = LocalMangaIndexEntity(
mangaId = manga.id, mangaId = manga.id,
path = file.path, path = file.path,
) )
private suspend fun computeHash(): Long { private fun isUpdateRequired() = currentVersion < VERSION
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
}
companion object { companion object {
private const val PREF_NAME = "_local_index" 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.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope 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.core.model.isLocal
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
abstract class LocalObserveMapper<E : Any, R : Any>( abstract class LocalObserveMapper<E : Any, R : Any>(
private val localMangaIndex: LocalMangaIndex, 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) val dispatcher = Dispatchers.IO.limitedParallelism(6)
map { item -> map { item ->
val m = toManga(item) val m = toManga(item)

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.LocaleComparator 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.sortedWithSafe
import org.koitharu.kotatsu.core.util.ext.toList import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.core.util.ext.toLocale import org.koitharu.kotatsu.core.util.ext.toLocale
@@ -43,15 +44,20 @@ class WelcomeViewModel @Inject constructor(
val types = MutableStateFlow( val types = MutableStateFlow(
FilterProperty( FilterProperty(
availableItems = ContentType.entries.toList(), availableItems = listOf(ContentType.MANGA),
selectedItems = setOf(ContentType.MANGA), selectedItems = setOf(ContentType.MANGA),
isLoading = false, isLoading = true,
error = null, error = null,
), ),
) )
init { init {
updateJob = launchJob(Dispatchers.Default) { 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 languages = localesGroups.keys.associateBy { x -> x.language }
val selectedLocales = HashSet<Locale>(2) val selectedLocales = HashSet<Locale>(2)
ConfigurationCompat.getLocales(context.resources.configuration).toList() 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.R
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.TABLE_SOURCES 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.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow 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.mapSortedByCount
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.data.SourcesSortOrder import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.EnumMap
import java.util.EnumSet import java.util.EnumSet
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@@ -139,13 +137,11 @@ class SourcesCatalogViewModel @Inject constructor(
@WorkerThread @WorkerThread
private fun getContentTypes(isNsfwDisabled: Boolean): List<ContentType> { private fun getContentTypes(isNsfwDisabled: Boolean): List<ContentType> {
val map = EnumMap<ContentType, Int>(ContentType::class.java) val result = repository.allMangaSources.mapSortedByCount { it.contentType }
for (e in MangaParserSource.entries) { return if (isNsfwDisabled) {
if (isNsfwDisabled && e.isNsfw()) { result.filterNot { it == ContentType.HENTAI }
continue } else {
} result
map[e.contentType] = map.getOrDefault(e.contentType, 0) + 1
} }
return map.entries.sortedByDescending { it.value }.map { it.key }
} }
} }