diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 31d3ffc16..b315e91c6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -273,6 +273,7 @@
+
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/BaseApp.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/BaseApp.kt
index 4a31e64b7..fb37b2cb6 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/BaseApp.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/BaseApp.kt
@@ -11,6 +11,7 @@ import androidx.work.Configuration
import androidx.work.WorkManager
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.acra.ACRA
@@ -28,6 +29,9 @@ import org.koitharu.kotatsu.core.os.AppValidator
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
+import org.koitharu.kotatsu.local.data.LocalStorageChanges
+import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
+import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
import java.security.Security
import javax.inject.Inject
@@ -60,6 +64,13 @@ open class BaseApp : Application(), Configuration.Provider {
@Inject
lateinit var workManagerProvider: Provider
+ @Inject
+ lateinit var localMangaIndexProvider: Provider
+
+ @Inject
+ @LocalStorageChanges
+ lateinit var localStorageChanges: MutableSharedFlow
+
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
@@ -82,6 +93,7 @@ open class BaseApp : Application(), Configuration.Provider {
}
processLifecycleScope.launch(Dispatchers.Default) {
setupDatabaseObservers()
+ localStorageChanges.collect(localMangaIndexProvider.get())
}
workScheduleManager.init()
WorkServiceStopHelper(workManagerProvider).setup()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt
index 1bec0c9b8..60f1abb02 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt
@@ -35,6 +35,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration19To20
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration20To21
import org.koitharu.kotatsu.core.db.migrations.Migration21To22
+import org.koitharu.kotatsu.core.db.migrations.Migration22To23
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
@@ -50,6 +51,8 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.FavouritesDao
import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity
+import org.koitharu.kotatsu.local.data.index.LocalMangaIndexDao
+import org.koitharu.kotatsu.local.data.index.LocalMangaIndexEntity
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingDao
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
import org.koitharu.kotatsu.stats.data.StatsDao
@@ -60,14 +63,14 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao
-const val DATABASE_VERSION = 22
+const val DATABASE_VERSION = 23
@Database(
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
- ScrobblingEntity::class, MangaSourceEntity::class, StatsEntity::class,
+ ScrobblingEntity::class, MangaSourceEntity::class, StatsEntity::class, LocalMangaIndexEntity::class,
],
version = DATABASE_VERSION,
)
@@ -98,6 +101,8 @@ abstract class MangaDatabase : RoomDatabase() {
abstract fun getSourcesDao(): MangaSourcesDao
abstract fun getStatsDao(): StatsDao
+
+ abstract fun getLocalMangaIndexDao(): LocalMangaIndexDao
}
fun getDatabaseMigrations(context: Context): Array = arrayOf(
@@ -122,6 +127,7 @@ fun getDatabaseMigrations(context: Context): Array = arrayOf(
Migration19To20(),
Migration20To21(),
Migration21To22(),
+ Migration22To23(),
)
fun MangaDatabase(context: Context): MangaDatabase = Room
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration22To23.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration22To23.kt
new file mode 100644
index 000000000..3ead2c4ff
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration22To23.kt
@@ -0,0 +1,11 @@
+package org.koitharu.kotatsu.core.db.migrations
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+class Migration22To23 : Migration(22, 23) {
+
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS `local_index` (`manga_id` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt
index c678d25d4..c62b7f807 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt
@@ -71,6 +71,7 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.TempFileFilter
+import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.domain.MangaLock
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
index b8e444a2d..fe62f0334 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
@@ -199,6 +199,7 @@ abstract class FavouritesDao : MangaQueryBuilder.ConditionCallback {
ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = favourites.manga_id) > 0"
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})"
+ ListFilterOption.Downloaded -> "EXISTS(SELECT * FROM local_index WHERE local_index.manga_id = favourites.manga_id)"
else -> null
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
index 85f654492..1e0d91693 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
@@ -42,7 +42,7 @@ class FavouritesRepository @Inject constructor(
fun observeAll(order: ListSortOrder, filterOptions: Set, limit: Int): Flow> {
if (ListFilterOption.Downloaded in filterOptions) {
- return localObserver.observeAll(order, filterOptions - ListFilterOption.Downloaded, limit)
+ return localObserver.observeAll(order, filterOptions, limit)
}
return db.getFavouritesDao().observeAll(order, filterOptions, limit)
.mapItems { it.toManga() }
@@ -60,7 +60,7 @@ class FavouritesRepository @Inject constructor(
limit: Int
): Flow> {
if (ListFilterOption.Downloaded in filterOptions) {
- return localObserver.observeAll(categoryId, order, filterOptions - ListFilterOption.Downloaded, limit)
+ return localObserver.observeAll(categoryId, order, filterOptions, limit)
}
return db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit)
.mapItems { it.toManga() }
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 0bee8280c..6fcacd4d2 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,29 +2,30 @@ 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
import org.koitharu.kotatsu.favourites.data.FavouriteManga
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder
-import org.koitharu.kotatsu.local.data.LocalMangaRepository
+import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.domain.LocalObserveMapper
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@Reusable
class LocalFavoritesObserver @Inject constructor(
- localMangaRepository: LocalMangaRepository,
+ localMangaIndex: LocalMangaIndex,
private val db: MangaDatabase,
-) : LocalObserveMapper(localMangaRepository, limitStep = 10) {
+) : LocalObserveMapper(localMangaIndex, limitStep = 10) {
fun observeAll(
order: ListSortOrder,
filterOptions: Set,
limit: Int
- ): Flow> = observe(limit) { newLimit ->
- db.getFavouritesDao().observeAll(order, filterOptions, newLimit)
+ ): Flow> = db.getFavouritesDao().observeAll(order, filterOptions, limit).mapLatest {
+ it.mapToLocal()
}
fun observeAll(
@@ -32,8 +33,8 @@ class LocalFavoritesObserver @Inject constructor(
order: ListSortOrder,
filterOptions: Set,
limit: Int
- ): Flow> = observe(limit) { newLimit ->
- db.getFavouritesDao().observeAll(categoryId, order, filterOptions, newLimit)
+ ): Flow> = db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit).mapLatest {
+ it.mapToLocal()
}
override fun toManga(e: FavouriteManga) = e.manga.toManga(e.tags.toMangaTags())
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt
index 1389e6524..23e0db88e 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt
@@ -159,6 +159,7 @@ abstract class HistoryDao : MangaQueryBuilder.ConditionCallback {
ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)"
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE history.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})"
+ ListFilterOption.Downloaded -> "EXISTS(SELECT * FROM local_index WHERE local_index.manga_id = history.manga_id)"
else -> null
}
}
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 e1324a2a3..00a95dc28 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,29 +1,30 @@
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
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder
-import org.koitharu.kotatsu.local.data.LocalMangaRepository
+import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.domain.LocalObserveMapper
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@Reusable
class HistoryLocalObserver @Inject constructor(
- localMangaRepository: LocalMangaRepository,
+ localMangaIndex: LocalMangaIndex,
private val db: MangaDatabase,
-) : LocalObserveMapper(localMangaRepository, limitStep = 10) {
+) : LocalObserveMapper(localMangaIndex, limitStep = 10) {
fun observeAll(
order: ListSortOrder,
filterOptions: Set,
limit: Int
- ) = observe(limit) { newLimit ->
- db.getHistoryDao().observeAll(order, filterOptions, newLimit)
+ ) = db.getHistoryDao().observeAll(order, filterOptions, limit).mapLatest {
+ it.mapToLocal()
}
override fun toManga(e: HistoryWithManga) = e.manga.toManga(e.tags.toMangaTags())
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt
index 03ccd91e5..6e323bd32 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt
@@ -82,7 +82,7 @@ class HistoryRepository @Inject constructor(
limit: Int
): Flow> {
if (ListFilterOption.Downloaded in filterOptions) {
- return localObserver.observeAll(order, filterOptions - ListFilterOption.Downloaded, limit)
+ return localObserver.observeAll(order, filterOptions, limit)
}
return db.getHistoryDao().observeAll(order, filterOptions, limit).mapItems {
MangaWithHistory(
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaMappingCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaMappingCache.kt
deleted file mode 100644
index 7ef175a0d..000000000
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaMappingCache.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.koitharu.kotatsu.local.data
-
-import androidx.collection.MutableLongObjectMap
-import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
-import org.koitharu.kotatsu.local.data.input.LocalMangaInput
-import org.koitharu.kotatsu.local.domain.model.LocalManga
-import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
-import java.io.File
-
-class LocalMangaMappingCache {
-
- private val map = MutableLongObjectMap()
-
- suspend fun get(mangaId: Long): LocalManga? {
- val file = synchronized(this) {
- map[mangaId]
- } ?: return null
- return runCatchingCancellable {
- LocalMangaInput.of(file).getManga()
- }.onFailure {
- it.printStackTraceDebug()
- }.getOrNull()
- }
-
- operator fun set(mangaId: Long, localManga: LocalManga?) = synchronized(this) {
- if (localManga == null) {
- map.remove(mangaId)
- } else {
- map[mangaId] = localManga.file
- }
- }
-}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt
index 54f8f0030..d43bf2805 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt
@@ -3,12 +3,11 @@ package org.koitharu.kotatsu.local.data
import android.net.Uri
import androidx.core.net.toFile
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.MutableSharedFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.LocalMangaSource
@@ -20,6 +19,7 @@ import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.filterWith
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
+import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.data.output.LocalMangaUtil
@@ -43,13 +43,13 @@ private const val MAX_PARALLELISM = 4
@Singleton
class LocalMangaRepository @Inject constructor(
private val storageManager: LocalStorageManager,
+ private val localMangaIndex: LocalMangaIndex,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow,
private val settings: AppSettings,
private val lock: MangaLock,
) : MangaRepository {
override val source = LocalMangaSource
- private val localMappingCache = LocalMangaMappingCache()
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
@@ -116,6 +116,7 @@ class LocalMangaRepository @Inject constructor(
val file = Uri.parse(manga.url).toFile()
val result = file.deleteAwait()
if (result) {
+ localMangaIndex.delete(manga.id)
localStorageChanges.emit(null)
}
return result
@@ -139,7 +140,7 @@ class LocalMangaRepository @Inject constructor(
suspend fun findSavedManga(remoteManga: Manga): LocalManga? = runCatchingCancellable {
// very fast path
- localMappingCache.get(remoteManga.id)?.let {
+ localMangaIndex.get(remoteManga.id)?.let {
return@runCatchingCancellable it
}
// fast path
@@ -164,7 +165,9 @@ class LocalMangaRepository @Inject constructor(
}
}.firstOrNull()?.getManga()
}.onSuccess { x: LocalManga? ->
- localMappingCache[remoteManga.id] = x
+ if (x != null) {
+ localMangaIndex.put(x)
+ }
}.onFailure {
it.printStackTraceDebug()
}.getOrNull()
@@ -199,18 +202,21 @@ class LocalMangaRepository @Inject constructor(
return true
}
- private suspend fun getRawList(): ArrayList {
- val files = getAllFiles().toList() // TODO remove toList()
- return coroutineScope {
- val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
- files.map { file ->
- async(dispatcher) {
- runCatchingCancellable { LocalMangaInput.ofOrNull(file)?.getManga() }.getOrNull()
+ fun getRawListAsFlow(): Flow = channelFlow {
+ val files = getAllFiles()
+ val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
+ for (file in files) {
+ launch(dispatcher) {
+ val m = LocalMangaInput.ofOrNull(file)?.getManga()
+ if (m != null) {
+ send(m)
}
- }.awaitAll()
- }.filterNotNullTo(ArrayList(files.size))
+ }
+ }
}
+ private suspend fun getRawList(): ArrayList = getRawListAsFlow().toCollection(ArrayList())
+
private suspend fun getAllFiles() = storageManager.getReadableDirs()
.asSequence()
.flatMap { dir ->
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
new file mode 100644
index 000000000..9a9c08f43
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt
@@ -0,0 +1,100 @@
+package org.koitharu.kotatsu.local.data.index
+
+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 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.util.runCatchingCancellable
+import java.io.File
+import javax.inject.Inject
+import javax.inject.Provider
+import javax.inject.Singleton
+
+@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 {
+
+ private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+
+ private var previousHash: Long
+ get() = prefs.getLong(KEY_HASH, 0L)
+ set(value) = prefs.edit { putLong(KEY_HASH, value) }
+
+ override suspend fun emit(value: LocalManga?) {
+ if (value != null) {
+ put(value)
+ }
+ }
+
+ suspend fun update(): Boolean {
+ val newHash = computeHash()
+ if (newHash == previousHash) {
+ return false
+ }
+ db.withTransaction {
+ val dao = db.getLocalMangaIndexDao()
+ dao.clear()
+ localMangaRepositoryProvider.get().getRawListAsFlow()
+ .collect { dao.upsert(it.toEntity()) }
+ }
+ previousHash = newHash
+ return true
+ }
+
+ suspend fun get(mangaId: Long): LocalManga? {
+ val path = db.getLocalMangaIndexDao().findPath(mangaId) ?: return null
+ return runCatchingCancellable {
+ LocalMangaInput.of(File(path)).getManga()
+ }.onFailure {
+ it.printStackTraceDebug()
+ }.getOrNull()
+ }
+
+ suspend fun put(manga: LocalManga) = db.withTransaction {
+ mangaDataRepository.storeManga(manga.manga)
+ db.getLocalMangaIndexDao().upsert(manga.toEntity())
+ }
+
+ suspend fun delete(mangaId: Long) {
+ db.getLocalMangaIndexDao().delete(mangaId)
+ }
+
+ 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
+ }
+
+ companion object {
+
+ private const val PREF_NAME = "_local_index"
+ private const val KEY_HASH = "hash"
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt
new file mode 100644
index 000000000..909a0458c
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt
@@ -0,0 +1,22 @@
+package org.koitharu.kotatsu.local.data.index
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Upsert
+import org.koitharu.kotatsu.core.db.entity.TagEntity
+
+@Dao
+interface LocalMangaIndexDao {
+
+ @Query("SELECT path FROM local_index WHERE manga_id = :mangaId")
+ suspend fun findPath(mangaId: Long): String?
+
+ @Upsert
+ suspend fun upsert(entity: LocalMangaIndexEntity)
+
+ @Query("DELETE FROM local_index WHERE manga_id = :mangaId")
+ suspend fun delete(mangaId: Long)
+
+ @Query("DELETE FROM local_index")
+ suspend fun clear()
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexEntity.kt
new file mode 100644
index 000000000..32e159c66
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexEntity.kt
@@ -0,0 +1,24 @@
+package org.koitharu.kotatsu.local.data.index
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.PrimaryKey
+import org.koitharu.kotatsu.core.db.entity.MangaEntity
+
+@Entity(
+ tableName = "local_index",
+ foreignKeys = [
+ ForeignKey(
+ entity = MangaEntity::class,
+ parentColumns = ["manga_id"],
+ childColumns = ["manga_id"],
+ onDelete = ForeignKey.CASCADE,
+ ),
+ ],
+)
+class LocalMangaIndexEntity(
+ @PrimaryKey(autoGenerate = false)
+ @ColumnInfo(name = "manga_id") val mangaId: Long,
+ @ColumnInfo(name = "path") val path: String,
+)
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 0e94293ea..91525dd26 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
@@ -1,56 +1,29 @@
package org.koitharu.kotatsu.local.domain
-import kotlinx.coroutines.CompletableDeferred
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.MutableStateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.transformLatest
import org.koitharu.kotatsu.core.model.isLocal
-import org.koitharu.kotatsu.local.data.LocalMangaRepository
+import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.parsers.model.Manga
-import java.util.Collections
-import java.util.WeakHashMap
abstract class LocalObserveMapper(
- private val localMangaRepository: LocalMangaRepository,
+ private val localMangaIndex: LocalMangaIndex,
private val limitStep: Int,
) {
- private val cache = Collections.synchronizedMap(WeakHashMap())
-
- protected fun observe(limit: Int, observer: (limit: Int) -> Flow>): Flow> {
- val floatingLimit = MutableStateFlow(limit)
- return floatingLimit.flatMapLatest { l ->
- observer(l)
- .transformLatest { fullList ->
- val mapped = fullList.mapToLocal(cache)
- if (mapped.size < limit && fullList.size == l) {
- floatingLimit.value += limitStep
- } else {
- emit(mapped.take(limit))
- }
- }.distinctUntilChanged()
- }
- }
-
- private suspend fun List.mapToLocal(cache: MutableMap): List = coroutineScope {
- val dispatcher = Dispatchers.IO.limitedParallelism(8)
+ protected suspend fun List.mapToLocal(): List = coroutineScope {
+ val dispatcher = Dispatchers.IO.limitedParallelism(6)
map { item ->
val m = toManga(item)
- if (cache.contains(m)) {
- CompletableDeferred(cache[m])
- } else async(dispatcher) {
+ async(dispatcher) {
val mapped = if (m.isLocal) {
m
} else {
- localMangaRepository.findSavedManga(m)?.manga
+ localMangaIndex.get(m.id)?.manga
}
- mapped?.let { mm -> toResult(item, mm) }.also { cache[m] = it }
+ mapped?.let { mm -> toResult(item, mm) }
}
}.awaitAll().filterNotNull()
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalIndexUpdateService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalIndexUpdateService.kt
new file mode 100644
index 000000000..72e287b17
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalIndexUpdateService.kt
@@ -0,0 +1,20 @@
+package org.koitharu.kotatsu.local.ui
+
+import android.content.Intent
+import dagger.hilt.android.AndroidEntryPoint
+import org.koitharu.kotatsu.core.ui.CoroutineIntentService
+import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class LocalIndexUpdateService : CoroutineIntentService() {
+
+ @Inject
+ lateinit var localMangaIndex: LocalMangaIndex
+
+ override suspend fun processIntent(startId: Int, intent: Intent) {
+ localMangaIndex.update()
+ }
+
+ override fun onError(startId: Int, error: Throwable) = Unit
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt
index 3360ab460..afd073a9b 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt
@@ -55,6 +55,7 @@ import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.favourites.ui.container.FavouritesContainerFragment
import org.koitharu.kotatsu.history.ui.HistoryListFragment
+import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
@@ -351,6 +352,7 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav
MangaPrefetchService.prefetchLast(this@MainActivity)
requestNotificationsPermission()
}
+ startService(Intent(this@MainActivity, LocalMangaIndex::class.java))
}
}