Store manga sources in database #426
This commit is contained in:
@@ -17,7 +17,7 @@ class MangaDatabaseTest {
|
||||
MangaDatabase::class.java,
|
||||
)
|
||||
|
||||
private val migrations = databaseMigrations
|
||||
private val migrations = getDatabaseMigrations()
|
||||
|
||||
@Test
|
||||
fun versions() {
|
||||
|
||||
@@ -12,11 +12,13 @@ import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarksDao
|
||||
import org.koitharu.kotatsu.core.db.dao.MangaDao
|
||||
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
|
||||
import org.koitharu.kotatsu.core.db.dao.PreferencesDao
|
||||
import org.koitharu.kotatsu.core.db.dao.TagsDao
|
||||
import org.koitharu.kotatsu.core.db.dao.TrackLogsDao
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration10To11
|
||||
@@ -25,6 +27,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration12To13
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration15To16
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration16To17
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||
@@ -49,14 +52,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 = 16
|
||||
const val DATABASE_VERSION = 17
|
||||
|
||||
@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,
|
||||
ScrobblingEntity::class, MangaSourceEntity::class,
|
||||
],
|
||||
version = DATABASE_VERSION,
|
||||
)
|
||||
@@ -83,30 +86,32 @@ abstract class MangaDatabase : RoomDatabase() {
|
||||
abstract val bookmarksDao: BookmarksDao
|
||||
|
||||
abstract val scrobblingDao: ScrobblingDao
|
||||
|
||||
abstract val sourcesDao: MangaSourcesDao
|
||||
}
|
||||
|
||||
val databaseMigrations: Array<Migration>
|
||||
get() = arrayOf(
|
||||
Migration1To2(),
|
||||
Migration2To3(),
|
||||
Migration3To4(),
|
||||
Migration4To5(),
|
||||
Migration5To6(),
|
||||
Migration6To7(),
|
||||
Migration7To8(),
|
||||
Migration8To9(),
|
||||
Migration9To10(),
|
||||
Migration10To11(),
|
||||
Migration11To12(),
|
||||
Migration12To13(),
|
||||
Migration13To14(),
|
||||
Migration14To15(),
|
||||
Migration15To16(),
|
||||
)
|
||||
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
|
||||
Migration1To2(),
|
||||
Migration2To3(),
|
||||
Migration3To4(),
|
||||
Migration4To5(),
|
||||
Migration5To6(),
|
||||
Migration6To7(),
|
||||
Migration7To8(),
|
||||
Migration8To9(),
|
||||
Migration9To10(),
|
||||
Migration10To11(),
|
||||
Migration11To12(),
|
||||
Migration12To13(),
|
||||
Migration13To14(),
|
||||
Migration14To15(),
|
||||
Migration15To16(),
|
||||
Migration16To17(context),
|
||||
)
|
||||
|
||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||
.databaseBuilder(context, MangaDatabase::class.java, "kotatsu-db")
|
||||
.addMigrations(*databaseMigrations)
|
||||
.addMigrations(*getDatabaseMigrations(context))
|
||||
.addCallback(DatabasePrePopulateCallback(context.resources))
|
||||
.build()
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
|
||||
@Dao
|
||||
abstract class MangaSourcesDao {
|
||||
|
||||
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||
abstract suspend fun findAll(): List<MangaSourceEntity>
|
||||
|
||||
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
|
||||
abstract suspend fun findAllEnabled(): List<MangaSourceEntity>
|
||||
|
||||
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
|
||||
abstract fun observeEnabled(): Flow<List<MangaSourceEntity>>
|
||||
|
||||
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
||||
|
||||
@Query("SELECT MAX(sort_key) FROM sources")
|
||||
abstract suspend fun getMaxSortKey(): Int
|
||||
|
||||
@Query("UPDATE sources SET enabled = 0")
|
||||
abstract suspend fun disableAllSources()
|
||||
|
||||
@Query("UPDATE sources SET sort_key = :sortKey WHERE source = :source")
|
||||
abstract suspend fun setSortKey(source: String, sortKey: Int)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
@Transaction
|
||||
abstract suspend fun insertIfAbsent(entries: Iterable<MangaSourceEntity>)
|
||||
|
||||
@Upsert
|
||||
abstract suspend fun upsert(entry: MangaSourceEntity)
|
||||
|
||||
@Transaction
|
||||
open suspend fun setEnabled(source: String, isEnabled: Boolean) {
|
||||
if (updateIsEnabled(source, isEnabled) == 0) {
|
||||
val entity = MangaSourceEntity(
|
||||
source = source,
|
||||
isEnabled = isEnabled,
|
||||
sortKey = getMaxSortKey() + 1,
|
||||
)
|
||||
upsert(entity)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("UPDATE sources SET enabled = :isEnabled WHERE source = :source")
|
||||
protected abstract suspend fun updateIsEnabled(source: String, isEnabled: Boolean): Int
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "sources",
|
||||
)
|
||||
data class MangaSourceEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "source")
|
||||
val source: String,
|
||||
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
|
||||
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class Migration16To17(context: Context) : Migration(16, 17) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE `sources` (`source` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, PRIMARY KEY(`source`))")
|
||||
database.execSQL("CREATE INDEX `index_sources_sort_key` ON `sources` (`sort_key`)")
|
||||
val hiddenSources = prefs.getStringSet("sources_hidden", null).orEmpty()
|
||||
val order = prefs.getString("sources_order_2", null)?.split('|').orEmpty()
|
||||
val sources = MangaSource.values()
|
||||
for (source in sources) {
|
||||
if (source == MangaSource.LOCAL) {
|
||||
continue
|
||||
}
|
||||
val name = source.name
|
||||
var sortKey = order.indexOf(name)
|
||||
if (sortKey == -1) {
|
||||
sortKey = order.size + source.ordinal
|
||||
}
|
||||
database.execSQL(
|
||||
"INSERT INTO `sources` (`source`, `enabled`, `sort_key`) VALUES (?, ?, ?)",
|
||||
arrayOf(name, (name !in hiddenSources).toInt(), sortKey),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Boolean.toInt() = if (this) 1 else 0
|
||||
}
|
||||
@@ -15,24 +15,19 @@ import androidx.core.os.LocaleListCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.json.JSONArray
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.core.util.ext.filterToSet
|
||||
import org.koitharu.kotatsu.core.util.ext.getEnumValue
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.putEnumValue
|
||||
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.io.File
|
||||
import java.net.Proxy
|
||||
import java.util.Collections
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -43,16 +38,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val connectivityManager = context.connectivityManager
|
||||
|
||||
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
|
||||
remove(MangaSource.LOCAL)
|
||||
if (!BuildConfig.DEBUG) {
|
||||
remove(MangaSource.DUMMY)
|
||||
}
|
||||
}
|
||||
|
||||
val remoteMangaSources: Set<MangaSource>
|
||||
get() = Collections.unmodifiableSet(remoteSources)
|
||||
|
||||
var listMode: ListMode
|
||||
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
|
||||
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
|
||||
@@ -183,37 +168,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
return policy.isNetworkAllowed(connectivityManager)
|
||||
}
|
||||
|
||||
var sourcesOrder: List<String>
|
||||
get() = prefs.getString(KEY_SOURCES_ORDER, null)
|
||||
?.split('|')
|
||||
.orEmpty()
|
||||
set(value) = prefs.edit {
|
||||
putString(KEY_SOURCES_ORDER, value.joinToString("|"))
|
||||
}
|
||||
|
||||
var hiddenSources: Set<String>
|
||||
get() = prefs.getStringSet(KEY_SOURCES_HIDDEN, null)?.filterToSet { name ->
|
||||
remoteSources.any { it.name == name }
|
||||
}.orEmpty()
|
||||
set(value) = prefs.edit { putStringSet(KEY_SOURCES_HIDDEN, value) }
|
||||
|
||||
val isSourcesSelected: Boolean
|
||||
get() = KEY_SOURCES_HIDDEN in prefs
|
||||
|
||||
val newSources: Set<MangaSource>
|
||||
get() {
|
||||
val known = sourcesOrder.toSet()
|
||||
val hidden = hiddenSources
|
||||
return remoteMangaSources
|
||||
.filterNotTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
|
||||
x.name in known || x.name in hidden
|
||||
}
|
||||
}
|
||||
|
||||
fun markKnownSources(sources: Collection<MangaSource>) {
|
||||
sourcesOrder = (sourcesOrder + sources.map { it.name }).distinct()
|
||||
}
|
||||
|
||||
var isSourcesGridMode: Boolean
|
||||
get() = prefs.getBoolean(KEY_SOURCES_GRID, false)
|
||||
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
||||
@@ -335,20 +289,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
return policy.isNetworkAllowed(connectivityManager)
|
||||
}
|
||||
|
||||
fun getMangaSources(includeHidden: Boolean): List<MangaSource> {
|
||||
val list = remoteSources.toMutableList()
|
||||
val order = sourcesOrder
|
||||
list.sortBy { x ->
|
||||
val e = order.indexOf(x.name)
|
||||
if (e == -1) order.size + x.ordinal else e
|
||||
}
|
||||
if (!includeHidden) {
|
||||
val hidden = hiddenSources
|
||||
list.removeAll { x -> x.name in hidden }
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun isTipEnabled(tip: String): Boolean {
|
||||
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
|
||||
}
|
||||
@@ -417,8 +357,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_THEME = "theme"
|
||||
const val KEY_COLOR_THEME = "color_theme"
|
||||
const val KEY_THEME_AMOLED = "amoled_theme"
|
||||
const val KEY_SOURCES_ORDER = "sources_order_2"
|
||||
const val KEY_SOURCES_HIDDEN = "sources_hidden"
|
||||
const val KEY_TRAFFIC_WARNING = "traffic_warning"
|
||||
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
|
||||
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package org.koitharu.kotatsu.explore.data
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import dagger.Reusable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.move
|
||||
import java.util.Collections
|
||||
import java.util.EnumSet
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class MangaSourcesRepository @Inject constructor(
|
||||
private val db: MangaDatabase,
|
||||
) {
|
||||
|
||||
private val dao: MangaSourcesDao
|
||||
get() = db.sourcesDao
|
||||
|
||||
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
|
||||
remove(MangaSource.LOCAL)
|
||||
if (!BuildConfig.DEBUG) {
|
||||
remove(MangaSource.DUMMY)
|
||||
}
|
||||
}
|
||||
|
||||
val allMangaSources: Set<MangaSource>
|
||||
get() = Collections.unmodifiableSet(remoteSources)
|
||||
|
||||
suspend fun getEnabledSources(): List<MangaSource> {
|
||||
return dao.findAllEnabled().toSources()
|
||||
}
|
||||
|
||||
fun observeEnabledSources(): Flow<List<MangaSource>> = dao.observeEnabled().map {
|
||||
it.toSources()
|
||||
}
|
||||
|
||||
fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities ->
|
||||
val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size)
|
||||
for (entity in entities) {
|
||||
val source = MangaSource(entity.source)
|
||||
if (source in remoteSources) {
|
||||
result.add(source to entity.isEnabled)
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
suspend fun setSourceEnabled(source: MangaSource, isEnabled: Boolean): ReversibleHandle {
|
||||
dao.setEnabled(source.name, isEnabled)
|
||||
return ReversibleHandle {
|
||||
dao.setEnabled(source.name, !isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setSourcesEnabled(sources: Iterable<MangaSource>, isEnabled: Boolean) {
|
||||
db.withTransaction {
|
||||
for (s in sources) {
|
||||
dao.setEnabled(s.name, isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun disableAllSources() {
|
||||
db.withTransaction {
|
||||
assimilateNewSources()
|
||||
dao.disableAllSources()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setPosition(source: MangaSource, index: Int) {
|
||||
db.withTransaction {
|
||||
val all = dao.findAll().toMutableList()
|
||||
val sourceIndex = all.indexOfFirst { x -> x.source == source.name }
|
||||
if (sourceIndex !in all.indices) {
|
||||
val entity = MangaSourceEntity(
|
||||
source = source.name,
|
||||
isEnabled = false,
|
||||
sortKey = index,
|
||||
)
|
||||
all.add(index, entity)
|
||||
dao.upsert(entity)
|
||||
} else {
|
||||
all.move(sourceIndex, index)
|
||||
}
|
||||
for ((i, e) in all.withIndex()) {
|
||||
if (e.sortKey != i) {
|
||||
dao.setSortKey(e.source, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun observeNewSources(): Flow<Set<MangaSource>> = dao.observeAll().map { entities ->
|
||||
val result = EnumSet.copyOf(remoteSources)
|
||||
for (e in entities) {
|
||||
result.remove(MangaSource(e.source))
|
||||
}
|
||||
result
|
||||
}.distinctUntilChanged()
|
||||
|
||||
suspend fun getNewSources(): Set<MangaSource> {
|
||||
val entities = dao.findAll()
|
||||
val result = EnumSet.copyOf(remoteSources)
|
||||
for (e in entities) {
|
||||
result.remove(MangaSource(e.source))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun assimilateNewSources(): Set<MangaSource> {
|
||||
val new = getNewSources()
|
||||
if (new.isEmpty()) {
|
||||
return emptySet()
|
||||
}
|
||||
var maxSortKey = dao.getMaxSortKey()
|
||||
val entities = new.map { x ->
|
||||
MangaSourceEntity(
|
||||
source = x.name,
|
||||
isEnabled = false,
|
||||
sortKey = ++maxSortKey,
|
||||
)
|
||||
}
|
||||
dao.insertIfAbsent(entities)
|
||||
return new
|
||||
}
|
||||
|
||||
suspend fun isSetupRequired(): Boolean {
|
||||
return dao.findAll().isEmpty()
|
||||
}
|
||||
|
||||
private fun List<MangaSourceEntity>.toSources(): List<MangaSource> {
|
||||
val result = ArrayList<MangaSource>(size)
|
||||
for (entity in this) {
|
||||
val source = MangaSource(entity.source)
|
||||
if (source in remoteSources) {
|
||||
result.add(source)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,18 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.almostEquals
|
||||
import org.koitharu.kotatsu.core.util.ext.asArrayList
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExploreRepository @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) {
|
||||
@@ -23,7 +25,7 @@ class ExploreRepository @Inject constructor(
|
||||
val tags = historyRepository.getPopularTags(tagsLimit).mapNotNull {
|
||||
if (it in blacklistTagRegex) null else it.title
|
||||
}
|
||||
val sources = settings.getMangaSources(includeHidden = false)
|
||||
val sources = sourcesRepository.getEnabledSources()
|
||||
check(sources.isNotEmpty()) { "No sources available" }
|
||||
for (i in 0..4) {
|
||||
val list = getList(sources.random(), tags, blacklistTagRegex)
|
||||
|
||||
@@ -7,13 +7,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -22,9 +18,9 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
|
||||
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
|
||||
@@ -45,6 +41,7 @@ class ExploreViewModel @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val suggestionRepository: SuggestionRepository,
|
||||
private val exploreRepository: ExploreRepository,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val isGrid = settings.observeAsStateFlow(
|
||||
@@ -96,10 +93,7 @@ class ExploreViewModel @Inject constructor(
|
||||
|
||||
fun hideSource(source: MangaSource) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
settings.hiddenSources += source.name
|
||||
val rollback = ReversibleHandle {
|
||||
settings.hiddenSources -= source.name
|
||||
}
|
||||
val rollback = sourcesRepository.setSourceEnabled(source, isEnabled = false)
|
||||
onActionDone.call(ReversibleAction(R.string.source_disabled, rollback))
|
||||
}
|
||||
}
|
||||
@@ -114,11 +108,11 @@ class ExploreViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun createContentFlow() = combine(
|
||||
observeSources(),
|
||||
sourcesRepository.observeEnabledSources(),
|
||||
getSuggestionFlow(),
|
||||
isGrid,
|
||||
isRandomLoading,
|
||||
observeNewSources(),
|
||||
sourcesRepository.observeNewSources(),
|
||||
) { content, suggestions, grid, randomLoading, newSources ->
|
||||
buildList(content, suggestions, grid, randomLoading, newSources)
|
||||
}
|
||||
@@ -160,15 +154,6 @@ class ExploreViewModel @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
private fun observeSources() = settings.observe()
|
||||
.filter {
|
||||
it == AppSettings.KEY_SOURCES_HIDDEN ||
|
||||
it == AppSettings.KEY_SOURCES_ORDER ||
|
||||
it == AppSettings.KEY_SUGGESTIONS
|
||||
}
|
||||
.onStart { emit("") }
|
||||
.map { settings.getMangaSources(includeHidden = false) }
|
||||
|
||||
private fun getLoadingStateList() = listOf(
|
||||
ExploreButtons(isRandomLoading.value),
|
||||
LoadingState,
|
||||
@@ -184,12 +169,6 @@ class ExploreViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeNewSources() = settings.observe()
|
||||
.filter { it == AppSettings.KEY_SOURCES_ORDER || it == AppSettings.KEY_SOURCES_HIDDEN }
|
||||
.onStart { emit("") }
|
||||
.map { settings.newSources }
|
||||
.distinctUntilChanged()
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TIP_SUGGESTIONS = "suggestions"
|
||||
|
||||
@@ -63,7 +63,6 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.about.AppUpdateDialog
|
||||
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
|
||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
@@ -136,6 +135,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
|
||||
viewModel.counters.observe(this, ::onCountersChanged)
|
||||
viewModel.appUpdate.observe(this) { invalidateMenu() }
|
||||
viewModel.onFirstStart.observeEvent(this) { OnboardDialogFragment.showWelcome(supportFragmentManager) }
|
||||
viewModel.isFeedAvailable.observe(this, ::onFeedAvailabilityChanged)
|
||||
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
|
||||
}
|
||||
@@ -324,15 +324,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
|
||||
private fun onFirstStart() {
|
||||
lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher
|
||||
when {
|
||||
!settings.isSourcesSelected -> withResumed {
|
||||
OnboardDialogFragment.showWelcome(supportFragmentManager)
|
||||
}
|
||||
|
||||
settings.newSources.isNotEmpty() -> withResumed {
|
||||
NewSourcesDialogFragment.show(supportFragmentManager)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Default) {
|
||||
LocalStorageCleanupWorker.enqueue(applicationContext)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||
@@ -18,6 +16,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.main.domain.ReadingResumeEnabledUseCase
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -31,9 +30,11 @@ class MainViewModel @Inject constructor(
|
||||
trackingRepository: TrackingRepository,
|
||||
private val settings: AppSettings,
|
||||
readingResumeEnabledUseCase: ReadingResumeEnabledUseCase,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onOpenReader = MutableEventFlow<Manga>()
|
||||
val onFirstStart = MutableEventFlow<Unit>()
|
||||
|
||||
val isResumeEnabled = readingResumeEnabledUseCase().stateIn(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
@@ -64,6 +65,11 @@ class MainViewModel @Inject constructor(
|
||||
launchJob {
|
||||
appUpdateRepository.fetchUpdate()
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
if (sourcesRepository.isSetupRequired()) {
|
||||
onFirstStart.call(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openLastReader() {
|
||||
@@ -77,10 +83,7 @@ class MainViewModel @Inject constructor(
|
||||
settings.isIncognitoModeEnabled = isEnabled
|
||||
}
|
||||
|
||||
private fun observeNewSourcesCount() = settings.observe()
|
||||
.filter { it == AppSettings.KEY_SOURCES_ORDER || it == AppSettings.KEY_SOURCES_HIDDEN }
|
||||
.onStart { emit("") }
|
||||
.map { settings.newSources.size }
|
||||
private fun observeNewSourcesCount() = sourcesRepository.observeNewSources()
|
||||
.map { it.size }
|
||||
.distinctUntilChanged()
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTag
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
@@ -26,27 +26,28 @@ import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class MangaSearchRepository @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val db: MangaDatabase,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val recentSuggestions: SearchRecentSuggestions,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) {
|
||||
|
||||
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
|
||||
settings.getMangaSources(includeHidden = false).asFlow()
|
||||
.flatMapMerge(concurrency) { source ->
|
||||
runCatchingCancellable {
|
||||
mangaRepositoryFactory.create(source).getList(
|
||||
offset = 0,
|
||||
query = query,
|
||||
)
|
||||
}.getOrElse {
|
||||
emptyList()
|
||||
}.asFlow()
|
||||
}.filter {
|
||||
match(it, query)
|
||||
}
|
||||
flow {
|
||||
emitAll(sourcesRepository.getEnabledSources().asFlow())
|
||||
}.flatMapMerge(concurrency) { source ->
|
||||
runCatchingCancellable {
|
||||
mangaRepositoryFactory.create(source).getList(
|
||||
offset = 0,
|
||||
query = query,
|
||||
)
|
||||
}.getOrElse {
|
||||
emptyList()
|
||||
}.asFlow()
|
||||
}.filter {
|
||||
match(it, query)
|
||||
}
|
||||
|
||||
suspend fun getMangaSuggestion(query: String, limit: Int, source: MangaSource?): List<Manga> {
|
||||
if (query.isEmpty()) {
|
||||
@@ -101,7 +102,7 @@ class MangaSearchRepository @Inject constructor(
|
||||
if (query.length < 3) {
|
||||
return emptyList()
|
||||
}
|
||||
val sources = settings.remoteMangaSources
|
||||
val sources = sourcesRepository.allMangaSources
|
||||
.filter { x -> x.title.contains(query, ignoreCase = true) }
|
||||
return if (limit == 0) {
|
||||
sources
|
||||
|
||||
@@ -19,12 +19,13 @@ import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
@@ -33,7 +34,6 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val MAX_PARALLELISM = 4
|
||||
@@ -43,9 +43,9 @@ private const val MIN_HAS_MORE_ITEMS = 8
|
||||
class MultiSearchViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val extraProvider: ListExtraProvider,
|
||||
private val settings: AppSettings,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private var searchJob: Job? = null
|
||||
@@ -117,7 +117,7 @@ class MultiSearchViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun searchImpl(q: String) = coroutineScope {
|
||||
val sources = settings.getMangaSources(includeHidden = false)
|
||||
val sources = sourcesRepository.getEnabledSources()
|
||||
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
|
||||
val deferredList = sources.map { source ->
|
||||
async(dispatcher) {
|
||||
|
||||
@@ -11,18 +11,20 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||
import java.util.EnumSet
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val DEBOUNCE_TIMEOUT = 500L
|
||||
@@ -35,6 +37,7 @@ private const val MAX_SOURCES_ITEMS = 6
|
||||
class SearchSuggestionViewModel @Inject constructor(
|
||||
private val repository: MangaSearchRepository,
|
||||
private val settings: AppSettings,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val query = MutableStateFlow("")
|
||||
@@ -72,10 +75,8 @@ class SearchSuggestionViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun onSourceToggle(source: MangaSource, isEnabled: Boolean) {
|
||||
settings.hiddenSources = if (isEnabled) {
|
||||
settings.hiddenSources - source.name
|
||||
} else {
|
||||
settings.hiddenSources + source.name
|
||||
launchJob(Dispatchers.Default) {
|
||||
sourcesRepository.setSourceEnabled(source, isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +91,10 @@ class SearchSuggestionViewModel @Inject constructor(
|
||||
suggestionJob?.cancel()
|
||||
suggestionJob = combine(
|
||||
query.debounce(DEBOUNCE_TIMEOUT),
|
||||
settings.observeAsFlow(AppSettings.KEY_SOURCES_HIDDEN) { hiddenSources },
|
||||
sourcesRepository.observeEnabledSources().map { EnumSet.copyOf(it) },
|
||||
::Pair,
|
||||
).mapLatest { (searchQuery, hiddenSources) ->
|
||||
buildSearchSuggestion(searchQuery, hiddenSources)
|
||||
).mapLatest { (searchQuery, enabledSources) ->
|
||||
buildSearchSuggestion(searchQuery, enabledSources)
|
||||
}.distinctUntilChanged()
|
||||
.onEach {
|
||||
suggestion.value = it
|
||||
@@ -102,7 +103,7 @@ class SearchSuggestionViewModel @Inject constructor(
|
||||
|
||||
private suspend fun buildSearchSuggestion(
|
||||
searchQuery: String,
|
||||
hiddenSources: Set<String>,
|
||||
enabledSources: Set<MangaSource>,
|
||||
): List<SearchSuggestionItem> = coroutineScope {
|
||||
val queriesDeferred = async {
|
||||
repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS)
|
||||
@@ -127,7 +128,7 @@ class SearchSuggestionViewModel @Inject constructor(
|
||||
add(SearchSuggestionItem.MangaList(mangaList))
|
||||
}
|
||||
queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
||||
sources.mapTo(this) { SearchSuggestionItem.Source(it, it.name !in hiddenSources) }
|
||||
sources.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
|
||||
class RootSettingsFragment : BasePreferenceFragment(0), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@AndroidEntryPoint
|
||||
class RootSettingsFragment : BasePreferenceFragment(0) {
|
||||
|
||||
private val viewModel: RootSettingsViewModel by viewModels()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_root)
|
||||
@@ -22,23 +27,18 @@ class RootSettingsFragment : BasePreferenceFragment(0), SharedPreferences.OnShar
|
||||
bindPreferenceSummary("tracker", R.string.track_sources, R.string.notifications_settings)
|
||||
bindPreferenceSummary("services", R.string.suggestions, R.string.sync, R.string.tracking)
|
||||
findPreference<Preference>("about")?.summary = getString(R.string.app_version, BuildConfig.VERSION_NAME)
|
||||
bindRemoteSourcesSummary()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_SOURCES_HIDDEN -> {
|
||||
bindRemoteSourcesSummary()
|
||||
findPreference<Preference>(AppSettings.KEY_REMOTE_SOURCES)?.let { pref ->
|
||||
val total = viewModel.totalSourcesCount
|
||||
viewModel.enabledSourcesCount.observe(viewLifecycleOwner) {
|
||||
pref.summary = if (it >= 0) {
|
||||
getString(R.string.enabled_d_of_d, it, total)
|
||||
} else {
|
||||
resources.getQuantityString(R.plurals.items, total, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,11 +46,4 @@ class RootSettingsFragment : BasePreferenceFragment(0), SharedPreferences.OnShar
|
||||
private fun bindPreferenceSummary(key: String, @StringRes vararg items: Int) {
|
||||
findPreference<Preference>(key)?.summary = items.joinToString { getString(it) }
|
||||
}
|
||||
|
||||
private fun bindRemoteSourcesSummary() {
|
||||
findPreference<Preference>(AppSettings.KEY_REMOTE_SOURCES)?.run {
|
||||
val total = settings.remoteMangaSources.size
|
||||
summary = getString(R.string.enabled_d_of_d, total - settings.hiddenSources.size, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RootSettingsViewModel @Inject constructor(
|
||||
sourcesRepository: MangaSourcesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val totalSourcesCount = sourcesRepository.allMangaSources.size
|
||||
|
||||
val enabledSourcesCount = sourcesRepository.observeEnabledSources()
|
||||
.map { it.size }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, -1)
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import androidx.fragment.app.viewModels
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
@@ -39,8 +38,7 @@ class NewSourcesDialogFragment :
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.textViewTitle.setText(R.string.new_sources_text)
|
||||
|
||||
viewModel.sources.filterNotNull()
|
||||
.observe(viewLifecycleOwner) { adapter.items = it }
|
||||
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
|
||||
}
|
||||
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||
@@ -51,7 +49,6 @@ class NewSourcesDialogFragment :
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
viewModel.apply()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +1,43 @@
|
||||
package org.koitharu.kotatsu.settings.newsources
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.mapToSet
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NewSourcesViewModel @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val repository: MangaSourcesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val initialList = settings.newSources
|
||||
val sources = MutableStateFlow<List<SourceConfigItem>?>(null)
|
||||
private var listUpdateJob: Job? = null
|
||||
|
||||
init {
|
||||
listUpdateJob = launchJob(Dispatchers.Default) {
|
||||
sources.value = buildList()
|
||||
}
|
||||
private val newSources = SuspendLazy {
|
||||
repository.assimilateNewSources()
|
||||
}
|
||||
val content: StateFlow<List<SourceConfigItem>> = repository.observeAll()
|
||||
.map { sources ->
|
||||
val new = newSources.get()
|
||||
sources.mapNotNull { (source, enabled) ->
|
||||
if (source in new) {
|
||||
SourceConfigItem.SourceItem(source, enabled, source.getLocaleTitle(), false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
||||
|
||||
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
|
||||
val prevJob = listUpdateJob
|
||||
listUpdateJob = launchJob(Dispatchers.Default) {
|
||||
if (isEnabled) {
|
||||
settings.hiddenSources -= item.source.name
|
||||
} else {
|
||||
settings.hiddenSources += item.source.name
|
||||
}
|
||||
prevJob?.cancelAndJoin()
|
||||
val list = buildList()
|
||||
ensureActive()
|
||||
sources.value = list
|
||||
}
|
||||
}
|
||||
|
||||
fun apply() {
|
||||
settings.markKnownSources(initialList)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun buildList(): List<SourceConfigItem.SourceItem> {
|
||||
val locales = LocaleListCompat.getDefault().mapToSet { it.language }
|
||||
val pendingHidden = HashSet<String>()
|
||||
return initialList.map {
|
||||
val locale = it.locale
|
||||
val isEnabledByLocale = locale == null || locale in locales
|
||||
if (!isEnabledByLocale) {
|
||||
pendingHidden += it.name
|
||||
}
|
||||
SourceConfigItem.SourceItem(
|
||||
source = it,
|
||||
summary = it.getLocaleTitle(),
|
||||
isEnabled = isEnabledByLocale,
|
||||
isDraggable = false,
|
||||
)
|
||||
}.also {
|
||||
if (pendingHidden.isNotEmpty()) {
|
||||
settings.hiddenSources += pendingHidden
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.setSourceEnabled(item.source, isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,9 +66,9 @@ class OnboardDialogFragment :
|
||||
viewModel.setItemChecked(item.key, isChecked)
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> viewModel.apply()
|
||||
DialogInterface.BUTTON_POSITIVE -> dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package org.koitharu.kotatsu.settings.onboard
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.map
|
||||
import org.koitharu.kotatsu.core.util.ext.mapToSet
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
import java.util.Locale
|
||||
@@ -17,31 +18,34 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class OnboardViewModel @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val repository: MangaSourcesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val allSources = settings.remoteMangaSources
|
||||
|
||||
private val allSources = repository.allMangaSources
|
||||
private val locales = allSources.groupBy { it.locale }
|
||||
|
||||
private val selectedLocales = locales.keys.toMutableSet()
|
||||
|
||||
private val selectedLocales = HashSet<String?>()
|
||||
val list = MutableStateFlow<List<SourceLocale>?>(null)
|
||||
private var updateJob: Job
|
||||
|
||||
init {
|
||||
if (settings.isSourcesSelected) {
|
||||
selectedLocales.removeAll(settings.hiddenSources.mapNotNullToSet { x -> MangaSource(x).locale })
|
||||
} else {
|
||||
val deviceLocales = LocaleListCompat.getDefault().mapToSet { x ->
|
||||
x.language
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
if (repository.isSetupRequired()) {
|
||||
val deviceLocales = LocaleListCompat.getDefault().mapToSet { x ->
|
||||
x.language
|
||||
}
|
||||
selectedLocales.addAll(deviceLocales)
|
||||
if (selectedLocales.isEmpty()) {
|
||||
selectedLocales += "en"
|
||||
}
|
||||
selectedLocales += null
|
||||
} else {
|
||||
selectedLocales.addAll(
|
||||
repository.getEnabledSources().mapNotNullToSet { x -> x.locale },
|
||||
)
|
||||
}
|
||||
selectedLocales.retainAll(deviceLocales)
|
||||
if (selectedLocales.isEmpty()) {
|
||||
selectedLocales += "en"
|
||||
}
|
||||
selectedLocales += null
|
||||
rebuildList()
|
||||
repository.assimilateNewSources()
|
||||
}
|
||||
rebuildList()
|
||||
}
|
||||
|
||||
fun setItemChecked(key: String?, isChecked: Boolean) {
|
||||
@@ -51,17 +55,17 @@ class OnboardViewModel @Inject constructor(
|
||||
selectedLocales.remove(key)
|
||||
}
|
||||
if (isModified) {
|
||||
rebuildList()
|
||||
val prevJob = updateJob
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
prevJob.join()
|
||||
val sources = allSources.filter { x -> x.locale == key }
|
||||
repository.setSourcesEnabled(sources, isChecked)
|
||||
rebuildList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun apply() {
|
||||
settings.hiddenSources = allSources.filterNot { x ->
|
||||
x.locale in selectedLocales
|
||||
}.mapToSet { x -> x.name }
|
||||
settings.markKnownSources(settings.newSources)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun rebuildList() {
|
||||
list.value = locales.map { (key, srcs) ->
|
||||
val locale = if (key != null) {
|
||||
|
||||
@@ -3,25 +3,26 @@ package org.koitharu.kotatsu.settings.sources
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
||||
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.ui.util.ReversibleHandle
|
||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.map
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.move
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
import java.util.TreeMap
|
||||
import javax.inject.Inject
|
||||
@@ -34,6 +35,7 @@ private const val TIP_REORDER = "src_reorder"
|
||||
@HiltViewModel
|
||||
class SourcesListViewModel @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val repository: MangaSourcesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val items = MutableStateFlow<List<SourceConfigItem>>(emptyList())
|
||||
@@ -51,13 +53,19 @@ class SourcesListViewModel @Inject constructor(
|
||||
|
||||
fun reorderSources(oldPos: Int, newPos: Int): Boolean {
|
||||
val snapshot = items.value.toMutableList()
|
||||
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false
|
||||
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false
|
||||
val item = (snapshot[oldPos] as? SourceConfigItem.SourceItem) ?: return false
|
||||
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) return false
|
||||
launchAtomicJob(Dispatchers.Default) {
|
||||
snapshot.move(oldPos, newPos)
|
||||
settings.sourcesOrder = snapshot.mapNotNull {
|
||||
(it as? SourceConfigItem.SourceItem)?.source?.name
|
||||
var targetPosition = 0
|
||||
for ((i, x) in snapshot.withIndex()) {
|
||||
if (i == newPos) {
|
||||
break
|
||||
}
|
||||
if (x is SourceConfigItem.SourceItem) {
|
||||
targetPosition++
|
||||
}
|
||||
}
|
||||
repository.setPosition(item.source, targetPosition)
|
||||
buildList()
|
||||
}
|
||||
return true
|
||||
@@ -71,17 +79,8 @@ class SourcesListViewModel @Inject constructor(
|
||||
|
||||
fun setEnabled(source: MangaSource, isEnabled: Boolean) {
|
||||
launchAtomicJob(Dispatchers.Default) {
|
||||
settings.hiddenSources = if (isEnabled) {
|
||||
settings.hiddenSources - source.name
|
||||
} else {
|
||||
settings.hiddenSources + source.name
|
||||
}
|
||||
if (isEnabled) {
|
||||
settings.markKnownSources(setOf(source))
|
||||
} else {
|
||||
val rollback = ReversibleHandle {
|
||||
setEnabled(source, true)
|
||||
}
|
||||
val rollback = repository.setSourceEnabled(source, isEnabled)
|
||||
if (!isEnabled) {
|
||||
onActionDone.call(ReversibleAction(R.string.source_disabled, rollback))
|
||||
}
|
||||
buildList()
|
||||
@@ -90,9 +89,7 @@ class SourcesListViewModel @Inject constructor(
|
||||
|
||||
fun disableAll() {
|
||||
launchAtomicJob(Dispatchers.Default) {
|
||||
settings.hiddenSources = settings.getMangaSources(includeHidden = true).mapToSet {
|
||||
it.name
|
||||
}
|
||||
repository.disableAllSources()
|
||||
buildList()
|
||||
}
|
||||
}
|
||||
@@ -122,36 +119,37 @@ class SourcesListViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildList() = runInterruptible(Dispatchers.Default) {
|
||||
val sources = settings.getMangaSources(includeHidden = true)
|
||||
val hiddenSources = settings.hiddenSources
|
||||
private suspend fun buildList() = withContext(Dispatchers.Default) {
|
||||
val allSources = repository.allMangaSources
|
||||
val enabledSources = repository.getEnabledSources()
|
||||
val enabledSet = EnumSet.copyOf(enabledSources)
|
||||
val query = searchQuery
|
||||
if (!query.isNullOrEmpty()) {
|
||||
items.value = sources.mapNotNull {
|
||||
items.value = allSources.mapNotNull {
|
||||
if (!it.title.contains(query, ignoreCase = true)) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
SourceConfigItem.SourceItem(
|
||||
source = it,
|
||||
summary = it.getLocaleTitle(),
|
||||
isEnabled = it.name !in hiddenSources,
|
||||
isEnabled = it in enabledSet,
|
||||
isDraggable = false,
|
||||
)
|
||||
}.ifEmpty {
|
||||
listOf(SourceConfigItem.EmptySearchResult)
|
||||
}
|
||||
return@runInterruptible
|
||||
return@withContext
|
||||
}
|
||||
val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) {
|
||||
if (it.name !in hiddenSources) {
|
||||
val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) {
|
||||
if (it in enabledSet) {
|
||||
KEY_ENABLED
|
||||
} else {
|
||||
it.locale
|
||||
}
|
||||
}
|
||||
val result = ArrayList<SourceConfigItem>(sources.size + map.size + 2)
|
||||
val enabledSources = map.remove(KEY_ENABLED)
|
||||
if (!enabledSources.isNullOrEmpty()) {
|
||||
map.remove(KEY_ENABLED)
|
||||
val result = ArrayList<SourceConfigItem>(allSources.size + map.size + 2)
|
||||
if (enabledSources.isNotEmpty()) {
|
||||
result += SourceConfigItem.Header(R.string.enabled_sources)
|
||||
if (settings.isTipEnabled(TIP_REORDER)) {
|
||||
result += SourceConfigItem.Tip(TIP_REORDER, R.drawable.ic_tap_reorder, R.string.sources_reorder_tip)
|
||||
@@ -165,10 +163,11 @@ class SourcesListViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (enabledSources?.size != sources.size) {
|
||||
if (enabledSources.size != allSources.size) {
|
||||
result += SourceConfigItem.Header(R.string.available_sources)
|
||||
val comparator = compareBy<MangaSource, String>(AlphanumComparator()) { it.name }
|
||||
for ((key, list) in map) {
|
||||
list.sortBy { it.ordinal }
|
||||
list.sortWith(comparator)
|
||||
val isExpanded = key in expandedGroups
|
||||
result += SourceConfigItem.LocaleGroup(
|
||||
localeId = key,
|
||||
@@ -195,12 +194,12 @@ class SourcesListViewModel @Inject constructor(
|
||||
return locale.getDisplayLanguage(locale).toTitleCase(locale)
|
||||
}
|
||||
|
||||
private inline fun launchAtomicJob(
|
||||
private fun launchAtomicJob(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
crossinline block: suspend CoroutineScope.() -> Unit
|
||||
) = launchJob(context) {
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) = launchJob(start = CoroutineStart.ATOMIC) {
|
||||
mutex.withLock {
|
||||
block()
|
||||
withContext(context, block)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ import org.koitharu.kotatsu.core.util.ext.takeMostFrequent
|
||||
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
|
||||
import org.koitharu.kotatsu.core.util.ext.trySetForeground
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -79,6 +80,7 @@ class SuggestionsWorker @AssistedInject constructor(
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val appSettings: AppSettings,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
@@ -128,7 +130,7 @@ class SuggestionsWorker @AssistedInject constructor(
|
||||
historyRepository.getList(0, 20) +
|
||||
favouritesRepository.getLastManga(20)
|
||||
).distinctById()
|
||||
val sources = appSettings.getMangaSources(includeHidden = false)
|
||||
val sources = sourcesRepository.getEnabledSources()
|
||||
if (seed.isEmpty() || sources.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user