Option to hide nsfw content
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user