Store manga sources in database #426

This commit is contained in:
Koitharu
2023-07-24 15:47:52 +03:00
parent 03b92c4898
commit 376cee1859
22 changed files with 478 additions and 311 deletions

View File

@@ -17,7 +17,7 @@ class MangaDatabaseTest {
MangaDatabase::class.java,
)
private val migrations = databaseMigrations
private val migrations = getDatabaseMigrations()
@Test
fun versions() {

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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,
)

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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) }
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}

View File

@@ -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) {

View File

@@ -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)
}
}

View File

@@ -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
}