Option to enable all sources

This commit is contained in:
Koitharu
2025-01-02 15:38:02 +02:00
parent 70d66e5a90
commit 2762caaa8f
16 changed files with 127 additions and 48 deletions

View File

@@ -2,6 +2,7 @@ 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 org.koitharu.kotatsu.core.db.entity.ChapterEntity
@@ -24,6 +25,6 @@ abstract class ChaptersDao {
insert(entities)
}
@Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun insert(entities: Collection<ChapterEntity>)
}

View File

@@ -10,7 +10,6 @@ import androidx.room.Upsert
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
@@ -61,21 +60,11 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources WHERE pinned = 1")
abstract suspend fun findAllPinned(): List<MangaSourceEntity>
fun observeEnabled(order: SourcesSortOrder): Flow<List<MangaSourceEntity>> {
val orderBy = getOrderBy(order)
fun observeAll(enabledOnly: Boolean, order: SourcesSortOrder): Flow<List<MangaSourceEntity>> =
observeImpl(getQuery(enabledOnly, order))
@Language("RoomSql")
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
return observeImpl(query)
}
suspend fun findAllEnabled(order: SourcesSortOrder): List<MangaSourceEntity> {
val orderBy = getOrderBy(order)
@Language("RoomSql")
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
return findAllImpl(query)
}
suspend fun findAll(enabledOnly: Boolean, order: SourcesSortOrder): List<MangaSourceEntity> =
findAllImpl(getQuery(enabledOnly, order))
@Transaction
open suspend fun setEnabled(source: String, isEnabled: Boolean) {
@@ -101,6 +90,17 @@ abstract class MangaSourcesDao {
@RawQuery
protected abstract suspend fun findAllImpl(query: SupportSQLiteQuery): List<MangaSourceEntity>
private fun getQuery(enabledOnly: Boolean, order: SourcesSortOrder) = SimpleSQLiteQuery(
buildString {
append("SELECT * FROM sources ")
if (enabledOnly) {
append("WHERE enabled = 1 ")
}
append("ORDER BY pinned DESC, ")
append(getOrderBy(order))
},
)
private fun getOrderBy(order: SourcesSortOrder) = when (order) {
SourcesSortOrder.ALPHABETIC -> "source ASC"
SourcesSortOrder.POPULARITY -> "(SELECT COUNT(*) FROM manga WHERE source = sources.source) DESC"

View File

@@ -299,6 +299,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getInt(KEY_SOURCES_VERSION, 0)
set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) }
var isAllSourcesEnabled: Boolean
get() = prefs.getBoolean(KEY_SOURCES_ENABLED_ALL, false)
set(value) = prefs.edit { putBoolean(KEY_SOURCES_ENABLED_ALL, value) }
val isPagesNumbersEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
@@ -717,6 +721,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_FEED_HEADER = "feed_header"
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
const val KEY_SOURCES_VERSION = "sources_version"
const val KEY_SOURCES_ENABLED_ALL = "sources_enabled_all"
const val KEY_QUICK_FILTER = "quick_filter"
const val KEY_BACKUP_TG_ENABLED = "backup_periodic_tg_enabled"
const val KEY_BACKUP_TG_CHAT = "backup_periodic_tg_chat_id"

View File

@@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.BuildConfig
@@ -61,13 +62,14 @@ class MangaSourcesRepository @Inject constructor(
suspend fun getEnabledSources(): List<MangaSource> {
assimilateNewSources()
val order = settings.sourcesSortOrder
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order).let { enabled ->
val external = getExternalSources()
val list = ArrayList<MangaSourceInfo>(enabled.size + external.size)
external.mapTo(list) { MangaSourceInfo(it, isEnabled = true, isPinned = true) }
list.addAll(enabled)
list
}
return dao.findAll(!settings.isAllSourcesEnabled, order).toSources(settings.isNsfwContentDisabled, order)
.let { enabled ->
val external = getExternalSources()
val list = ArrayList<MangaSourceInfo>(enabled.size + external.size)
external.mapTo(list) { MangaSourceInfo(it, isEnabled = true, isPinned = true) }
list.addAll(enabled)
list
}
}
suspend fun getPinnedSources(): Set<MangaSource> {
@@ -85,6 +87,9 @@ class MangaSourcesRepository @Inject constructor(
suspend fun getDisabledSources(): Set<MangaSource> {
assimilateNewSources()
if (settings.isAllSourcesEnabled) {
return emptySet()
}
val result = EnumSet.copyOf(allMangaSources)
val enabled = dao.findAllEnabledNames()
for (name in enabled) {
@@ -105,7 +110,7 @@ class MangaSourcesRepository @Inject constructor(
): List<MangaParserSource> {
assimilateNewSources()
val entities = dao.findAll().toMutableList()
if (isDisabledOnly) {
if (isDisabledOnly && !settings.isAllSourcesEnabled) {
entities.removeAll { it.isEnabled }
}
if (isNewOnly) {
@@ -141,7 +146,9 @@ class MangaSourcesRepository @Inject constructor(
fun observeEnabledSourcesCount(): Flow<Int> {
return combine(
observeIsNsfwDisabled(),
dao.observeEnabled(SourcesSortOrder.MANUAL),
observeAllEnabled().flatMapLatest { isAllSourcesEnabled ->
dao.observeAll(!isAllSourcesEnabled, SourcesSortOrder.MANUAL)
},
) { skipNsfw, sources ->
sources.count {
it.source.toMangaSourceOrNull()?.let { s -> !skipNsfw || !s.isNsfw() } == true
@@ -152,7 +159,9 @@ class MangaSourcesRepository @Inject constructor(
fun observeAvailableSourcesCount(): Flow<Int> {
return combine(
observeIsNsfwDisabled(),
dao.observeEnabled(SourcesSortOrder.MANUAL),
observeAllEnabled().flatMapLatest { isAllSourcesEnabled ->
dao.observeAll(!isAllSourcesEnabled, SourcesSortOrder.MANUAL)
},
) { skipNsfw, enabledSources ->
val enabled = enabledSources.mapToSet { it.source }
allMangaSources.count { x ->
@@ -163,9 +172,10 @@ class MangaSourcesRepository @Inject constructor(
fun observeEnabledSources(): Flow<List<MangaSourceInfo>> = combine(
observeIsNsfwDisabled(),
observeAllEnabled(),
observeSortOrder(),
) { skipNsfw, order ->
dao.observeEnabled(order).map {
) { skipNsfw, allEnabled, order ->
dao.observeAll(!allEnabled, order).map {
it.toSources(skipNsfw, order)
}
}.flattenLatest()
@@ -249,10 +259,11 @@ class MangaSourcesRepository @Inject constructor(
return false
}
var maxSortKey = dao.getMaxSortKey()
val isAllEnabled = settings.isAllSourcesEnabled
val entities = new.map { x ->
MangaSourceEntity(
source = x.name,
isEnabled = false,
isEnabled = isAllEnabled,
sortKey = ++maxSortKey,
addedIn = BuildConfig.VERSION_CODE,
lastUsedAt = 0,
@@ -355,6 +366,7 @@ class MangaSourcesRepository @Inject constructor(
skipNsfwSources: Boolean,
sortOrder: SourcesSortOrder?,
): MutableList<MangaSourceInfo> {
val isAllEnabled = settings.isAllSourcesEnabled
val result = ArrayList<MangaSourceInfo>(size)
for (entity in this) {
val source = entity.source.toMangaSourceOrNull() ?: continue
@@ -365,7 +377,7 @@ class MangaSourcesRepository @Inject constructor(
result.add(
MangaSourceInfo(
mangaSource = source,
isEnabled = entity.isEnabled,
isEnabled = entity.isEnabled || isAllEnabled,
isPinned = entity.isPinned,
),
)
@@ -385,5 +397,9 @@ class MangaSourcesRepository @Inject constructor(
sourcesSortOrder
}
private fun observeAllEnabled() = settings.observeAsFlow(AppSettings.KEY_SOURCES_ENABLED_ALL) {
isAllSourcesEnabled
}
private fun String.toMangaSourceOrNull(): MangaParserSource? = MangaParserSource.entries.find { it.name == this }
}

View File

@@ -112,6 +112,8 @@ class ExploreFragment :
override fun onListHeaderClick(item: ListHeader, view: View) {
if (item.payload == R.id.nav_suggestions) {
router.openSuggestions()
} else if (viewModel.isAllSourcesEnabled.value) {
router.openManageSources()
} else {
router.openSourcesCatalog()
}
@@ -166,7 +168,8 @@ class ExploreFragment :
menu.findItem(R.id.action_shortcut).isVisible = isSingleSelection
menu.findItem(R.id.action_pin).isVisible = selectedSources.all { !it.isPinned }
menu.findItem(R.id.action_unpin).isVisible = selectedSources.all { it.isPinned }
menu.findItem(R.id.action_disable)?.isVisible = selectedSources.all { it.mangaSource is MangaParserSource }
menu.findItem(R.id.action_disable)?.isVisible = !viewModel.isAllSourcesEnabled.value &&
selectedSources.all { it.mangaSource is MangaParserSource }
menu.findItem(R.id.action_delete)?.isVisible = selectedSources.all { it.mangaSource is ExternalMangaSource }
return super.onPrepareActionMode(controller, mode, menu)
}

View File

@@ -7,7 +7,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
@@ -23,6 +22,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.combine
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
@@ -54,6 +54,12 @@ class ExploreViewModel @Inject constructor(
valueProducer = { isSourcesGridMode },
)
val isAllSourcesEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.IO,
key = AppSettings.KEY_SOURCES_ENABLED_ALL,
valueProducer = { isAllSourcesEnabled },
)
private val isSuggestionsEnabled = settings.observeAsFlow(
key = AppSettings.KEY_SUGGESTIONS,
valueProducer = { isSuggestionsEnabled },
@@ -137,9 +143,10 @@ class ExploreViewModel @Inject constructor(
getSuggestionFlow(),
isGrid,
isRandomLoading,
isAllSourcesEnabled,
sourcesRepository.observeHasNewSourcesForBadge(),
) { content, suggestions, grid, randomLoading, newSources ->
buildList(content, suggestions, grid, randomLoading, newSources)
) { content, suggestions, grid, randomLoading, allSourcesEnabled, newSources ->
buildList(content, suggestions, grid, randomLoading, allSourcesEnabled, newSources)
}.withErrorHandling()
private fun buildList(
@@ -147,6 +154,7 @@ class ExploreViewModel @Inject constructor(
recommendation: List<Manga>,
isGrid: Boolean,
randomLoading: Boolean,
allSourcesEnabled: Boolean,
hasNewSources: Boolean,
): List<ListModel> {
val result = ArrayList<ListModel>(sources.size + 3)
@@ -158,8 +166,8 @@ class ExploreViewModel @Inject constructor(
if (sources.isNotEmpty()) {
result += ListHeader(
textRes = R.string.remote_sources,
buttonTextRes = R.string.catalog,
badge = if (hasNewSources) "" else null,
buttonTextRes = if (allSourcesEnabled) R.string.manage else R.string.catalog,
badge = if (!allSourcesEnabled && hasNewSources) "" else null,
)
sources.mapTo(result) { MangaSourceItem(it, isGrid) }
} else {

View File

@@ -42,7 +42,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0), Preference.OnPreferenc
val isValidSource = viewModel.repository !is EmptyMangaRepository
findPreference<SwitchPreferenceCompat>(KEY_ENABLE)?.run {
isVisible = isValidSource
isVisible = isValidSource && !settings.isAllSourcesEnabled
onPreferenceChangeListener = this@SourceSettingsFragment
}
findPreference<Preference>(KEY_AUTH)?.run {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.sources
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
@@ -17,7 +18,8 @@ import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.parsers.util.names
@AndroidEntryPoint
class SourcesSettingsFragment : BasePreferenceFragment(R.string.remote_sources) {
class SourcesSettingsFragment : BasePreferenceFragment(R.string.remote_sources),
SharedPreferences.OnSharedPreferenceChangeListener {
private val viewModel by viewModels<SourcesSettingsViewModel>()
@@ -43,10 +45,10 @@ class SourcesSettingsFragment : BasePreferenceFragment(R.string.remote_sources)
}
findPreference<Preference>(AppSettings.KEY_SOURCES_CATALOG)?.let { pref ->
viewModel.availableSourcesCount.observe(viewLifecycleOwner) {
pref.summary = if (it >= 0) {
getString(R.string.available_d, it)
} else {
null
pref.summary = when {
it == 0 -> getString(R.string.all_sources_enabled)
it > 0 -> getString(R.string.available_d, it)
else -> null
}
}
}
@@ -55,6 +57,13 @@ class SourcesSettingsFragment : BasePreferenceFragment(R.string.remote_sources)
pref.isChecked = it
}
}
updateEnableAllDependencies()
settings.subscribe(this)
}
override fun onDestroyView() {
settings.unsubscribe(this)
super.onDestroyView()
}
override fun onPreferenceTreeClick(preference: Preference): Boolean = when (preference.key) {
@@ -70,4 +79,14 @@ class SourcesSettingsFragment : BasePreferenceFragment(R.string.remote_sources)
else -> super.onPreferenceTreeClick(preference)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
AppSettings.KEY_SOURCES_ENABLED_ALL -> updateEnableAllDependencies()
}
}
private fun updateEnableAllDependencies() {
findPreference<Preference>(AppSettings.KEY_SOURCES_CATALOG)?.isEnabled = !settings.isAllSourcesEnabled
}
}

View File

@@ -58,7 +58,7 @@ fun sourceConfigItemDelegate2(
bind {
binding.textViewTitle.text = item.source.getTitle(context)
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
binding.imageViewRemove.isVisible = item.isEnabled
binding.imageViewRemove.isVisible = item.isEnabled && item.isDisableAvailable
binding.imageViewMenu.isVisible = item.isEnabled
binding.textViewTitle.drawableStart = if (item.isPinned) iconPinned else null
binding.textViewDescription.text = item.source.getSummary(context)

View File

@@ -70,6 +70,7 @@ class SourcesListProducer @Inject constructor(
val pinned = repository.getPinnedSources().mapToSet { it.name }
val isNsfwDisabled = settings.isNsfwContentDisabled
val isReorderAvailable = settings.sourcesSortOrder == SourcesSortOrder.MANUAL
val isDisableAvailable = !settings.isAllSourcesEnabled
val withTip = isReorderAvailable && settings.isTipEnabled(TIP_REORDER)
val enabledSet = enabledSources.toSet()
if (query.isNotEmpty()) {
@@ -83,6 +84,7 @@ class SourcesListProducer @Inject constructor(
isDraggable = false,
isAvailable = !isNsfwDisabled || !it.isNsfw(),
isPinned = it.name in pinned,
isDisableAvailable = isDisableAvailable,
)
}.ifEmpty {
listOf(SourceConfigItem.EmptySearchResult)
@@ -104,6 +106,7 @@ class SourcesListProducer @Inject constructor(
isDraggable = isReorderAvailable,
isAvailable = false,
isPinned = it.name in pinned,
isDisableAvailable = isDisableAvailable,
)
}
}

View File

@@ -171,6 +171,8 @@ class SourcesManageFragment :
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_no_nsfw).isChecked = settings.isNsfwContentDisabled
menu.findItem(R.id.action_disable_all).isVisible = !settings.isAllSourcesEnabled
menu.findItem(R.id.action_catalog).isVisible = !settings.isAllSourcesEnabled
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {

View File

@@ -14,6 +14,7 @@ sealed interface SourceConfigItem : ListModel {
val isDraggable: Boolean,
val isAvailable: Boolean,
val isPinned: Boolean,
val isDisableAvailable: Boolean,
) : SourceConfigItem {
val isNsfw: Boolean

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M22.11 21.46L2.39 1.73L1.11 3L4.06 5.95C2.78 7.63 2 9.72 2 12C2 17.5 6.5 22 12 22C14.28 22 16.37 21.23 18.05 19.94L20.84 22.73L22.11 21.46M12 20C7.58 20 4 16.42 4 12C4 10.27 4.56 8.68 5.5 7.38L16.62 18.5C15.32 19.45 13.73 20 12 20M8.17 4.97L6.72 3.5C8.25 2.56 10.06 2 12 2C17.5 2 22 6.5 22 12C22 13.94 21.44 15.75 20.5 17.28L19.03 15.83C19.65 14.69 20 13.39 20 12C20 7.58 16.42 4 12 4C10.61 4 9.31 4.35 8.17 4.97Z" />
</vector>

View File

@@ -82,10 +82,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/remove"
android:contentDescription="@string/disable"
android:padding="@dimen/margin_small"
android:scaleType="center"
android:src="@drawable/ic_delete"
android:tooltipText="@string/remove" />
android:src="@drawable/ic_disable"
android:tooltipText="@string/disable" />
</LinearLayout>

View File

@@ -789,4 +789,7 @@
<string name="open_telegram_bot_summary">Press to open chat with Kotatsu Backup Bot</string>
<string name="clear_database">Clear database</string>
<string name="clear_database_summary">Delete information about manga that is not used</string>
<string name="enable_all_sources">Enable all manga sources</string>
<string name="enable_all_sources_summary">All available manga sources will be enabled permanently</string>
<string name="all_sources_enabled">All sources are enabled</string>
</resources>

View File

@@ -20,11 +20,17 @@
android:persistent="false"
android:title="@string/manage_sources" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="sources_enabled_all"
android:summary="@string/enable_all_sources_summary"
android:title="@string/enable_all_sources"
app:allowDividerAbove="true" />
<Preference
android:key="sources_catalog"
android:persistent="false"
android:title="@string/sources_catalog"
app:allowDividerAbove="true" />
android:title="@string/sources_catalog" />
<SwitchPreferenceCompat
android:defaultValue="false"