diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt index d895efa5b..d98e67739 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt @@ -65,6 +65,7 @@ abstract class BaseViewModel : ViewModel() { } protected fun Flow.withErrorHandling() = catch { error -> + error.printStackTraceDebug() errorEvent.call(error) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt index 109d605a4..8af44c883 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt @@ -97,3 +97,14 @@ fun LongSet.toSet(): Set = toCollection(ArraySet(size)) fun > LongSet.toCollection(out: R): R = out.also { result -> forEach(result::add) } + +fun Collection.mapSortedByCount(isDescending: Boolean = true, mapper: (T) -> R): List { + val grouped = groupBy(mapper).toList() + val sortSelector: (Pair>) -> Int = { it.second.size } + val sorted = if (isDescending) { + grouped.sortedByDescending(sortSelector) + } else { + grouped.sortedBy(sortSelector) + } + return sorted.map { it.first } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt index 2ffbd8d9c..4ccdb344f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt @@ -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 - get() = Collections.unmodifiableSet(remoteSources) + val allMangaSources: Set = Collections.unmodifiableSet( + EnumSet.allOf(MangaParserSource::class.java).apply { + if (!BuildConfig.DEBUG) { + remove(MangaParserSource.DUMMY) + } + }, + ) suspend fun getEnabledSources(): List { assimilateNewSources() @@ -86,7 +84,7 @@ class MangaSourcesRepository @Inject constructor( suspend fun getDisabledSources(): Set { 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>(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) { 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 = 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 = combine( @@ -295,7 +293,7 @@ class MangaSourcesRepository @Inject constructor( private suspend fun getNewSources(): MutableSet { 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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/LocalFavoritesObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/LocalFavoritesObserver.kt index 6fcacd4d2..59162a210 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/LocalFavoritesObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/LocalFavoritesObserver.kt @@ -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(localMangaIndex, limitStep = 10) { +) : LocalObserveMapper(localMangaIndex) { fun observeAll( order: ListSortOrder, filterOptions: Set, limit: Int - ): Flow> = db.getFavouritesDao().observeAll(order, filterOptions, limit).mapLatest { - it.mapToLocal() - } + ): Flow> = db.getFavouritesDao().observeAll(order, filterOptions, limit).mapToLocal() fun observeAll( categoryId: Long, order: ListSortOrder, filterOptions: Set, limit: Int - ): Flow> = db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit).mapLatest { - it.mapToLocal() - } + ): Flow> = db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit).mapToLocal() override fun toManga(e: FavouriteManga) = e.manga.toManga(e.tags.toMangaTags()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryLocalObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryLocalObserver.kt index 00a95dc28..7deeea3c9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryLocalObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryLocalObserver.kt @@ -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(localMangaIndex, limitStep = 10) { +) : LocalObserveMapper(localMangaIndex) { fun observeAll( order: ListSortOrder, filterOptions: Set, 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()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt index e68067acf..e7b6757d6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt @@ -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, ) : FlowCollector { @@ -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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt index 91525dd26..38fe8ed21 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt @@ -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( private val localMangaIndex: LocalMangaIndex, - private val limitStep: Int, ) { - protected suspend fun List.mapToLocal(): List = coroutineScope { + protected fun Flow>.mapToLocal() = onStart { + localMangaIndex.updateIfRequired() + }.mapLatest { + it.mapToLocal() + } + + private suspend fun Collection.mapToLocal(): List = coroutineScope { val dispatcher = Dispatchers.IO.limitedParallelism(6) map { item -> val m = toManga(item) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt index 350fd8c67..7ec601e12 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt @@ -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(2) ConfigurationCompat.getLocales(context.resources.configuration).toList() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt index 0a190cc67..359e1dccd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt @@ -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 { - val map = EnumMap(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 } } }