Option to hide nsfw content

This commit is contained in:
Koitharu
2023-08-11 14:50:56 +03:00
parent 03cb458d92
commit caebca36de
12 changed files with 104 additions and 129 deletions

View File

@@ -9,7 +9,6 @@ import android.provider.Settings
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.collection.arraySetOf
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@@ -56,6 +55,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getInt(KEY_GRID_SIZE, 100) get() = prefs.getInt(KEY_GRID_SIZE, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) } set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
var isNsfwContentDisabled: Boolean
get() = prefs.getBoolean(KEY_DISABLE_NSFW, false)
set(value) = prefs.edit { putBoolean(KEY_DISABLE_NSFW, value) }
var appLocales: LocaleListCompat var appLocales: LocaleListCompat
get() { get() {
val raw = prefs.getString(KEY_APP_LOCALE, null) val raw = prefs.getString(KEY_APP_LOCALE, null)
@@ -444,6 +447,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PROXY_PASSWORD = "proxy_password" const val KEY_PROXY_PASSWORD = "proxy_password"
const val KEY_IMAGES_PROXY = "images_proxy" const val KEY_IMAGES_PROXY = "images_proxy"
const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs" const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs"
const val KEY_DISABLE_NSFW = "no_nsfw"
// About // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

View File

@@ -4,13 +4,17 @@ import androidx.room.withTransaction
import dagger.Reusable import dagger.Reusable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.move import org.koitharu.kotatsu.parsers.util.move
import java.util.Collections import java.util.Collections
@@ -20,6 +24,7 @@ import javax.inject.Inject
@Reusable @Reusable
class MangaSourcesRepository @Inject constructor( class MangaSourcesRepository @Inject constructor(
private val db: MangaDatabase, private val db: MangaDatabase,
private val settings: AppSettings,
) { ) {
private val dao: MangaSourcesDao private val dao: MangaSourcesDao
@@ -36,11 +41,13 @@ class MangaSourcesRepository @Inject constructor(
get() = Collections.unmodifiableSet(remoteSources) get() = Collections.unmodifiableSet(remoteSources)
suspend fun getEnabledSources(): List<MangaSource> { suspend fun getEnabledSources(): List<MangaSource> {
return dao.findAllEnabled().toSources() return dao.findAllEnabled().toSources(settings.isNsfwContentDisabled)
} }
fun observeEnabledSources(): Flow<List<MangaSource>> = dao.observeEnabled().map { fun observeEnabledSources(): Flow<List<MangaSource>> = observeIsNsfwDisabled().flatMapLatest { skipNsfw ->
it.toSources() dao.observeEnabled().map {
it.toSources(skipNsfw)
}
} }
fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities -> fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities ->
@@ -137,14 +144,21 @@ class MangaSourcesRepository @Inject constructor(
return dao.findAll().isEmpty() return dao.findAll().isEmpty()
} }
private fun List<MangaSourceEntity>.toSources(): List<MangaSource> { private fun List<MangaSourceEntity>.toSources(skipNsfwSources: Boolean): List<MangaSource> {
val result = ArrayList<MangaSource>(size) val result = ArrayList<MangaSource>(size)
for (entity in this) { for (entity in this) {
val source = MangaSource(entity.source) val source = MangaSource(entity.source)
if (skipNsfwSources && source.contentType == ContentType.HENTAI) {
continue
}
if (source in remoteSources) { if (source in remoteSources) {
result.add(source) result.add(source)
} }
} }
return result return result
} }
private fun observeIsNsfwDisabled() = settings.observeAsFlow(AppSettings.KEY_DISABLE_NSFW) {
isNsfwContentDisabled
}
} }

View File

@@ -19,7 +19,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
abstract class MangaListViewModel( abstract class MangaListViewModel(
settings: AppSettings, private val settings: AppSettings,
private val downloadScheduler: DownloadWorker.Scheduler, private val downloadScheduler: DownloadWorker.Scheduler,
) : BaseViewModel() { ) : BaseViewModel() {
@@ -46,4 +46,10 @@ abstract class MangaListViewModel(
onDownloadStarted.call(Unit) onDownloadStarted.call(Unit)
} }
} }
fun List<Manga>.skipNsfwIfNeeded() = if (settings.isNsfwContentDisabled) {
filterNot { it.isNsfw }
} else {
this
}
} }

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
@@ -66,7 +67,7 @@ open class RemoteListViewModel @Inject constructor(
private var randomJob: Job? = null private var randomJob: Job? = null
override val content = combine( override val content = combine(
mangaList, mangaList.map { it?.skipNsfwIfNeeded() },
listMode, listMode,
listError, listError,
hasNextPage, hasNextPage,

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.search.domain package org.koitharu.kotatsu.search.domain
import android.annotation.SuppressLint
import android.app.SearchManager import android.app.SearchManager
import android.content.Context import android.content.Context
import android.provider.SearchRecentSuggestions import android.provider.SearchRecentSuggestions
@@ -8,19 +7,18 @@ import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTag 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.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import javax.inject.Inject import javax.inject.Inject
@@ -30,34 +28,22 @@ class MangaSearchRepository @Inject constructor(
private val sourcesRepository: MangaSourcesRepository, private val sourcesRepository: MangaSourcesRepository,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val recentSuggestions: SearchRecentSuggestions, private val recentSuggestions: SearchRecentSuggestions,
private val mangaRepositoryFactory: MangaRepository.Factory, private val settings: AppSettings,
) { ) {
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
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> { suspend fun getMangaSuggestion(query: String, limit: Int, source: MangaSource?): List<Manga> {
if (query.isEmpty()) { if (query.isEmpty()) {
return emptyList() return emptyList()
} }
val skipNsfw = settings.isNsfwContentDisabled
return if (source != null) { return if (source != null) {
db.mangaDao.searchByTitle("%$query%", source.name, limit) db.mangaDao.searchByTitle("%$query%", source.name, limit)
} else { } else {
db.mangaDao.searchByTitle("%$query%", limit) db.mangaDao.searchByTitle("%$query%", limit)
}.map { it.toManga() } }.let {
if (skipNsfw) it.filterNot { x -> x.manga.isNsfw } else it
}
.map { it.toManga() }
.sortedBy { x -> x.title.levenshteinDistance(query) } .sortedBy { x -> x.title.levenshteinDistance(query) }
} }
@@ -67,7 +53,7 @@ class MangaSearchRepository @Inject constructor(
): List<String> = withContext(Dispatchers.IO) { ): List<String> = withContext(Dispatchers.IO) {
context.contentResolver.query( context.contentResolver.query(
MangaSuggestionsProvider.QUERY_URI, MangaSuggestionsProvider.QUERY_URI,
SUGGESTION_PROJECTION, arrayOf(SearchManager.SUGGEST_COLUMN_QUERY),
"${SearchManager.SUGGEST_COLUMN_QUERY} LIKE ?", "${SearchManager.SUGGEST_COLUMN_QUERY} LIKE ?",
arrayOf("%$query%"), arrayOf("%$query%"),
"date DESC", "date DESC",
@@ -102,8 +88,11 @@ class MangaSearchRepository @Inject constructor(
if (query.length < 3) { if (query.length < 3) {
return emptyList() return emptyList()
} }
val skipNsfw = settings.isNsfwContentDisabled
val sources = sourcesRepository.allMangaSources val sources = sourcesRepository.allMangaSources
.filter { x -> x.title.contains(query, ignoreCase = true) } .filter { x ->
(x.contentType != ContentType.HENTAI || !skipNsfw) && x.title.contains(query, ignoreCase = true)
}
return if (limit == 0) { return if (limit == 0) {
sources sources
} else { } else {
@@ -130,33 +119,10 @@ class MangaSearchRepository @Inject constructor(
suspend fun getSearchHistoryCount(): Int = withContext(Dispatchers.IO) { suspend fun getSearchHistoryCount(): Int = withContext(Dispatchers.IO) {
context.contentResolver.query( context.contentResolver.query(
MangaSuggestionsProvider.QUERY_URI, MangaSuggestionsProvider.QUERY_URI,
SUGGESTION_PROJECTION, arrayOf(SearchManager.SUGGEST_COLUMN_QUERY),
null, null,
arrayOfNulls(1), arrayOfNulls(1),
null, null,
)?.use { cursor -> cursor.count } ?: 0 )?.use { cursor -> cursor.count } ?: 0
} }
private companion object {
private val REGEX_SPACE = Regex("\\s+")
val SUGGESTION_PROJECTION = arrayOf(SearchManager.SUGGEST_COLUMN_QUERY)
@SuppressLint("DefaultLocale")
fun match(manga: Manga, query: String): Boolean {
val words = HashSet<String>()
words += manga.title.lowercase().split(REGEX_SPACE)
words += manga.altTitle?.lowercase()?.split(REGEX_SPACE).orEmpty()
val words2 = query.lowercase().split(REGEX_SPACE).toSet()
for (w in words) {
for (w2 in words2) {
val diff = w.levenshteinDistance(w2) / ((w.length + w2.length) / 2f)
if (diff < 0.5) {
return true
}
}
}
return false
}
}
} }

View File

@@ -9,8 +9,10 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.getLocaleTitle 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.BaseViewModel
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import javax.inject.Inject import javax.inject.Inject
@@ -18,6 +20,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class NewSourcesViewModel @Inject constructor( class NewSourcesViewModel @Inject constructor(
private val repository: MangaSourcesRepository, private val repository: MangaSourcesRepository,
private val settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
private val newSources = SuspendLazy { private val newSources = SuspendLazy {
@@ -26,9 +29,16 @@ class NewSourcesViewModel @Inject constructor(
val content: StateFlow<List<SourceConfigItem>> = repository.observeAll() val content: StateFlow<List<SourceConfigItem>> = repository.observeAll()
.map { sources -> .map { sources ->
val new = newSources.get() val new = newSources.get()
val skipNsfw = settings.isNsfwContentDisabled
sources.mapNotNull { (source, enabled) -> sources.mapNotNull { (source, enabled) ->
if (source in new) { if (source in new) {
SourceConfigItem.SourceItem(source, enabled, source.getLocaleTitle(), false) SourceConfigItem.SourceItem(
source = source,
isEnabled = enabled,
summary = source.getLocaleTitle(),
isDraggable = false,
isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI,
)
} else { } else {
null null
} }

View File

@@ -16,6 +16,7 @@ import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
@@ -41,6 +42,9 @@ class SourcesManageFragment :
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
private var reorderHelper: ItemTouchHelper? = null private var reorderHelper: ItemTouchHelper? = null
private val viewModel by viewModels<SourcesManageViewModel>() private val viewModel by viewModels<SourcesManageViewModel>()
@@ -128,9 +132,19 @@ class SourcesManageFragment :
true true
} }
R.id.action_no_nsfw -> {
settings.isNsfwContentDisabled = !menuItem.isChecked
true
}
else -> false else -> false
} }
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_no_nsfw).isChecked = settings.isNsfwContentDisabled
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true) (activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
return true return true

View File

@@ -27,6 +27,7 @@ import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.map import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.core.util.ext.toEnumSet import org.koitharu.kotatsu.core.util.ext.toEnumSet
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
@@ -54,8 +55,9 @@ class SourcesManageViewModel @Inject constructor(
expandedGroups, expandedGroups,
searchQuery, searchQuery,
observeTip(), observeTip(),
) { sources, groups, query, tip -> settings.observeAsFlow(AppSettings.KEY_DISABLE_NSFW) { isNsfwContentDisabled },
buildList(sources, groups, query, tip) ) { sources, groups, query, tip, noNsfw ->
buildList(sources, groups, query, tip, noNsfw)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
val onActionDone = MutableEventFlow<ReversibleAction>() val onActionDone = MutableEventFlow<ReversibleAction>()
@@ -125,6 +127,7 @@ class SourcesManageViewModel @Inject constructor(
expanded: Set<String?>, expanded: Set<String?>,
query: String?, query: String?,
withTip: Boolean, withTip: Boolean,
isNsfwDisabled: Boolean,
): List<SourceConfigItem> { ): List<SourceConfigItem> {
val allSources = repository.allMangaSources val allSources = repository.allMangaSources
val enabledSet = enabledSources.toEnumSet() val enabledSet = enabledSources.toEnumSet()
@@ -138,6 +141,7 @@ class SourcesManageViewModel @Inject constructor(
summary = it.getLocaleTitle(), summary = it.getLocaleTitle(),
isEnabled = it in enabledSet, isEnabled = it in enabledSet,
isDraggable = false, isDraggable = false,
isAvailable = !isNsfwDisabled || !it.isNsfw(),
) )
}.ifEmpty { }.ifEmpty {
listOf(SourceConfigItem.EmptySearchResult) listOf(SourceConfigItem.EmptySearchResult)
@@ -163,6 +167,7 @@ class SourcesManageViewModel @Inject constructor(
summary = it.getLocaleTitle(), summary = it.getLocaleTitle(),
isEnabled = true, isEnabled = true,
isDraggable = true, isDraggable = true,
isAvailable = false,
) )
} }
} }
@@ -184,6 +189,7 @@ class SourcesManageViewModel @Inject constructor(
summary = null, summary = null,
isEnabled = false, isEnabled = false,
isDraggable = false, isDraggable = false,
isAvailable = !isNsfwDisabled || !it.isNsfw(),
) )
} }
} }
@@ -210,6 +216,8 @@ class SourcesManageViewModel @Inject constructor(
isTipEnabled(TIP_REORDER) isTipEnabled(TIP_REORDER)
} }
private fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
private class LocaleKeyComparator : Comparator<String?> { private class LocaleKeyComparator : Comparator<String?> {
private val deviceLocales = LocaleListCompat.getAdjustedDefault() private val deviceLocales = LocaleListCompat.getAdjustedDefault()

View File

@@ -72,8 +72,17 @@ fun sourceConfigItemCheckableDelegate(
} }
bind { bind {
binding.textViewTitle.text = item.source.title binding.textViewTitle.text = if (item.isNsfw) {
buildSpannedString {
append(item.source.title)
append(' ')
appendNsfwLabel(context)
}
} else {
item.source.title
}
binding.switchToggle.isChecked = item.isEnabled binding.switchToggle.isChecked = item.isEnabled
binding.switchToggle.isEnabled = item.isAvailable
binding.textViewDescription.textAndVisible = item.summary binding.textViewDescription.textAndVisible = item.summary
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
@@ -120,7 +129,7 @@ fun sourceConfigItemDelegate2(
} else { } else {
item.source.title item.source.title
} }
binding.imageViewAdd.isGone = item.isEnabled binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
binding.imageViewRemove.isVisible = item.isEnabled binding.imageViewRemove.isVisible = item.isEnabled
binding.imageViewConfig.isVisible = item.isEnabled binding.imageViewConfig.isVisible = item.isEnabled
binding.textViewDescription.textAndVisible = item.summary binding.textViewDescription.textAndVisible = item.summary

View File

@@ -9,25 +9,16 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface SourceConfigItem : ListModel { sealed interface SourceConfigItem : ListModel {
class Header( data class Header(
@StringRes val titleResId: Int, @StringRes val titleResId: Int,
) : SourceConfigItem { ) : SourceConfigItem {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is Header && other.titleResId == titleResId return other is Header && other.titleResId == titleResId
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Header
return titleResId == other.titleResId
}
override fun hashCode(): Int = titleResId
} }
class LocaleGroup( data class LocaleGroup(
val localeId: String?, val localeId: String?,
val title: String?, val title: String?,
val isExpanded: Boolean, val isExpanded: Boolean,
@@ -44,31 +35,14 @@ sealed interface SourceConfigItem : ListModel {
super.getChangePayload(previousState) super.getChangePayload(previousState)
} }
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LocaleGroup
if (localeId != other.localeId) return false
if (title != other.title) return false
return isExpanded == other.isExpanded
}
override fun hashCode(): Int {
var result = localeId?.hashCode() ?: 0
result = 31 * result + (title?.hashCode() ?: 0)
result = 31 * result + isExpanded.hashCode()
return result
}
} }
class SourceItem( data class SourceItem(
val source: MangaSource, val source: MangaSource,
val isEnabled: Boolean, val isEnabled: Boolean,
val summary: String?, val summary: String?,
val isDraggable: Boolean, val isDraggable: Boolean,
val isAvailable: Boolean,
) : SourceConfigItem { ) : SourceConfigItem {
val isNsfw: Boolean val isNsfw: Boolean
@@ -85,29 +59,9 @@ sealed interface SourceConfigItem : ListModel {
super.getChangePayload(previousState) super.getChangePayload(previousState)
} }
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SourceItem
if (source != other.source) return false
if (summary != other.summary) return false
if (isEnabled != other.isEnabled) return false
return isDraggable == other.isDraggable
}
override fun hashCode(): Int {
var result = source.hashCode()
result = 31 * result + summary.hashCode()
result = 31 * result + isEnabled.hashCode()
result = 31 * result + isDraggable.hashCode()
return result
}
} }
class Tip( data class Tip(
val key: String, val key: String,
@DrawableRes val iconResId: Int, @DrawableRes val iconResId: Int,
@StringRes val textResId: Int, @StringRes val textResId: Int,
@@ -116,24 +70,6 @@ sealed interface SourceConfigItem : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is Tip && other.key == key return other is Tip && other.key == key
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Tip
if (key != other.key) return false
if (iconResId != other.iconResId) return false
return textResId == other.textResId
}
override fun hashCode(): Int {
var result = key.hashCode()
result = 31 * result + iconResId
result = 31 * result + textResId
return result
}
} }
object EmptySearchResult : SourceConfigItem { object EmptySearchResult : SourceConfigItem {

View File

@@ -10,6 +10,12 @@
app:actionViewClass="androidx.appcompat.widget.SearchView" app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" /> app:showAsAction="ifRoom|collapseActionView" />
<item
android:id="@+id/action_no_nsfw"
android:checkable="true"
android:title="@string/disable_nsfw"
app:showAsAction="never" />
<item <item
android:id="@+id/action_locales" android:id="@+id/action_locales"
android:title="@string/languages" android:title="@string/languages"

View File

@@ -472,4 +472,5 @@
<string name="languages">Languages</string> <string name="languages">Languages</string>
<string name="unknown">Unknown</string> <string name="unknown">Unknown</string>
<string name="in_progress">In progress</string> <string name="in_progress">In progress</string>
<string name="disable_nsfw">Disable NSFW</string>
</resources> </resources>