From caebca36deb31399e5b15025b11bcf31218513d4 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 11 Aug 2023 14:50:56 +0300 Subject: [PATCH] Option to hide nsfw content --- .../kotatsu/core/prefs/AppSettings.kt | 6 +- .../explore/data/MangaSourcesRepository.kt | 22 +++++- .../kotatsu/list/ui/MangaListViewModel.kt | 8 +- .../remotelist/ui/RemoteListViewModel.kt | 3 +- .../search/domain/MangaSearchRepository.kt | 62 ++++------------ .../newsources/NewSourcesViewModel.kt | 12 ++- .../settings/sources/SourcesManageFragment.kt | 14 ++++ .../sources/SourcesManageViewModel.kt | 12 ++- .../adapter/SourceConfigAdapterDelegates.kt | 13 +++- .../sources/model/SourceConfigItem.kt | 74 ++----------------- app/src/main/res/menu/opt_sources.xml | 6 ++ app/src/main/res/values/strings.xml | 1 + 12 files changed, 104 insertions(+), 129 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 0c711b879..615c7d482 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -9,7 +9,6 @@ import android.provider.Settings import androidx.annotation.FloatRange import androidx.appcompat.app.AppCompatDelegate import androidx.collection.ArraySet -import androidx.collection.arraySetOf import androidx.core.content.edit import androidx.core.os.LocaleListCompat import androidx.preference.PreferenceManager @@ -56,6 +55,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { get() = prefs.getInt(KEY_GRID_SIZE, 100) 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 get() { 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_IMAGES_PROXY = "images_proxy" const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs" + const val KEY_DISABLE_NSFW = "no_nsfw" // About const val KEY_APP_UPDATE = "app_update" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt index 1f03adf0a..9d7f65b22 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt @@ -4,13 +4,17 @@ import androidx.room.withTransaction import dagger.Reusable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest 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.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsFlow 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.util.move import java.util.Collections @@ -20,6 +24,7 @@ import javax.inject.Inject @Reusable class MangaSourcesRepository @Inject constructor( private val db: MangaDatabase, + private val settings: AppSettings, ) { private val dao: MangaSourcesDao @@ -36,11 +41,13 @@ class MangaSourcesRepository @Inject constructor( get() = Collections.unmodifiableSet(remoteSources) suspend fun getEnabledSources(): List { - return dao.findAllEnabled().toSources() + return dao.findAllEnabled().toSources(settings.isNsfwContentDisabled) } - fun observeEnabledSources(): Flow> = dao.observeEnabled().map { - it.toSources() + fun observeEnabledSources(): Flow> = observeIsNsfwDisabled().flatMapLatest { skipNsfw -> + dao.observeEnabled().map { + it.toSources(skipNsfw) + } } fun observeAll(): Flow>> = dao.observeAll().map { entities -> @@ -137,14 +144,21 @@ class MangaSourcesRepository @Inject constructor( return dao.findAll().isEmpty() } - private fun List.toSources(): List { + private fun List.toSources(skipNsfwSources: Boolean): List { val result = ArrayList(size) for (entity in this) { val source = MangaSource(entity.source) + if (skipNsfwSources && source.contentType == ContentType.HENTAI) { + continue + } if (source in remoteSources) { result.add(source) } } return result } + + private fun observeIsNsfwDisabled() = settings.observeAsFlow(AppSettings.KEY_DISABLE_NSFW) { + isNsfwContentDisabled + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 8c197fae9..500c31796 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -19,7 +19,7 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag abstract class MangaListViewModel( - settings: AppSettings, + private val settings: AppSettings, private val downloadScheduler: DownloadWorker.Scheduler, ) : BaseViewModel() { @@ -46,4 +46,10 @@ abstract class MangaListViewModel( onDownloadStarted.call(Unit) } } + + fun List.skipNsfwIfNeeded() = if (settings.isNsfwContentDisabled) { + filterNot { it.isNsfw } + } else { + this + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 4988f2711..84b58dfb3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus @@ -66,7 +67,7 @@ open class RemoteListViewModel @Inject constructor( private var randomJob: Job? = null override val content = combine( - mangaList, + mangaList.map { it?.skipNsfwIfNeeded() }, listMode, listError, hasNextPage, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 3ee371282..f6cc9e9ec 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.search.domain -import android.annotation.SuppressLint import android.app.SearchManager import android.content.Context import android.provider.SearchRecentSuggestions @@ -8,19 +7,18 @@ import dagger.Reusable import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.flow.* import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext 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.ContentType import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.util.levenshteinDistance -import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import javax.inject.Inject @@ -30,34 +28,22 @@ class MangaSearchRepository @Inject constructor( private val sourcesRepository: MangaSourcesRepository, @ApplicationContext private val context: Context, private val recentSuggestions: SearchRecentSuggestions, - private val mangaRepositoryFactory: MangaRepository.Factory, + private val settings: AppSettings, ) { - fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow = - 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 { if (query.isEmpty()) { return emptyList() } + val skipNsfw = settings.isNsfwContentDisabled return if (source != null) { db.mangaDao.searchByTitle("%$query%", source.name, limit) } else { 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) } } @@ -67,7 +53,7 @@ class MangaSearchRepository @Inject constructor( ): List = withContext(Dispatchers.IO) { context.contentResolver.query( MangaSuggestionsProvider.QUERY_URI, - SUGGESTION_PROJECTION, + arrayOf(SearchManager.SUGGEST_COLUMN_QUERY), "${SearchManager.SUGGEST_COLUMN_QUERY} LIKE ?", arrayOf("%$query%"), "date DESC", @@ -102,8 +88,11 @@ class MangaSearchRepository @Inject constructor( if (query.length < 3) { return emptyList() } + val skipNsfw = settings.isNsfwContentDisabled 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) { sources } else { @@ -130,33 +119,10 @@ class MangaSearchRepository @Inject constructor( suspend fun getSearchHistoryCount(): Int = withContext(Dispatchers.IO) { context.contentResolver.query( MangaSuggestionsProvider.QUERY_URI, - SUGGESTION_PROJECTION, + arrayOf(SearchManager.SUGGEST_COLUMN_QUERY), null, arrayOfNulls(1), null, )?.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() - 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 - } - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt index 1db085503..f9ca6be3c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt @@ -9,8 +9,10 @@ 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.explore.data.MangaSourcesRepository +import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import javax.inject.Inject @@ -18,6 +20,7 @@ import javax.inject.Inject @HiltViewModel class NewSourcesViewModel @Inject constructor( private val repository: MangaSourcesRepository, + private val settings: AppSettings, ) : BaseViewModel() { private val newSources = SuspendLazy { @@ -26,9 +29,16 @@ class NewSourcesViewModel @Inject constructor( val content: StateFlow> = repository.observeAll() .map { sources -> val new = newSources.get() + val skipNsfw = settings.isNsfwContentDisabled sources.mapNotNull { (source, enabled) -> 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 { null } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt index 861531338..7ca673d46 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt @@ -16,6 +16,7 @@ import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint 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.util.RecyclerViewOwner import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver @@ -41,6 +42,9 @@ class SourcesManageFragment : @Inject lateinit var coil: ImageLoader + @Inject + lateinit var settings: AppSettings + private var reorderHelper: ItemTouchHelper? = null private val viewModel by viewModels() @@ -128,9 +132,19 @@ class SourcesManageFragment : true } + R.id.action_no_nsfw -> { + settings.isNsfwContentDisabled = !menuItem.isChecked + true + } + 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 { (activity as? AppBarOwner)?.appBar?.setExpanded(false, true) return true diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt index 317f274e8..c1d24059c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt @@ -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.toEnumSet 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.util.toTitleCase import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem @@ -54,8 +55,9 @@ class SourcesManageViewModel @Inject constructor( expandedGroups, searchQuery, observeTip(), - ) { sources, groups, query, tip -> - buildList(sources, groups, query, tip) + settings.observeAsFlow(AppSettings.KEY_DISABLE_NSFW) { isNsfwContentDisabled }, + ) { sources, groups, query, tip, noNsfw -> + buildList(sources, groups, query, tip, noNsfw) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) val onActionDone = MutableEventFlow() @@ -125,6 +127,7 @@ class SourcesManageViewModel @Inject constructor( expanded: Set, query: String?, withTip: Boolean, + isNsfwDisabled: Boolean, ): List { val allSources = repository.allMangaSources val enabledSet = enabledSources.toEnumSet() @@ -138,6 +141,7 @@ class SourcesManageViewModel @Inject constructor( summary = it.getLocaleTitle(), isEnabled = it in enabledSet, isDraggable = false, + isAvailable = !isNsfwDisabled || !it.isNsfw(), ) }.ifEmpty { listOf(SourceConfigItem.EmptySearchResult) @@ -163,6 +167,7 @@ class SourcesManageViewModel @Inject constructor( summary = it.getLocaleTitle(), isEnabled = true, isDraggable = true, + isAvailable = false, ) } } @@ -184,6 +189,7 @@ class SourcesManageViewModel @Inject constructor( summary = null, isEnabled = false, isDraggable = false, + isAvailable = !isNsfwDisabled || !it.isNsfw(), ) } } @@ -210,6 +216,8 @@ class SourcesManageViewModel @Inject constructor( isTipEnabled(TIP_REORDER) } + private fun MangaSource.isNsfw() = contentType == ContentType.HENTAI + private class LocaleKeyComparator : Comparator { private val deviceLocales = LocaleListCompat.getAdjustedDefault() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt index 493950106..e8973929d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt @@ -72,8 +72,17 @@ fun sourceConfigItemCheckableDelegate( } 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.isEnabled = item.isAvailable binding.textViewDescription.textAndVisible = item.summary val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { @@ -120,7 +129,7 @@ fun sourceConfigItemDelegate2( } else { item.source.title } - binding.imageViewAdd.isGone = item.isEnabled + binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable binding.imageViewRemove.isVisible = item.isEnabled binding.imageViewConfig.isVisible = item.isEnabled binding.textViewDescription.textAndVisible = item.summary diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt index fc3a231b5..d1feb36db 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt @@ -9,25 +9,16 @@ import org.koitharu.kotatsu.parsers.model.MangaSource sealed interface SourceConfigItem : ListModel { - class Header( + data class Header( @StringRes val titleResId: Int, ) : SourceConfigItem { override fun areItemsTheSame(other: ListModel): Boolean { 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 title: String?, val isExpanded: Boolean, @@ -44,31 +35,14 @@ sealed interface SourceConfigItem : ListModel { 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 isEnabled: Boolean, val summary: String?, val isDraggable: Boolean, + val isAvailable: Boolean, ) : SourceConfigItem { val isNsfw: Boolean @@ -85,29 +59,9 @@ sealed interface SourceConfigItem : ListModel { 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, @DrawableRes val iconResId: Int, @StringRes val textResId: Int, @@ -116,24 +70,6 @@ sealed interface SourceConfigItem : ListModel { override fun areItemsTheSame(other: ListModel): Boolean { 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 { diff --git a/app/src/main/res/menu/opt_sources.xml b/app/src/main/res/menu/opt_sources.xml index 75a01436f..1a550d464 100644 --- a/app/src/main/res/menu/opt_sources.xml +++ b/app/src/main/res/menu/opt_sources.xml @@ -10,6 +10,12 @@ app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="ifRoom|collapseActionView" /> + + Languages Unknown In progress + Disable NSFW