Ask for one-time incognito for nsfw manga

This commit is contained in:
Koitharu
2025-05-03 10:55:17 +03:00
parent 842ecaaff6
commit 52c39ad40c
18 changed files with 128 additions and 46 deletions

View File

@@ -123,7 +123,7 @@ class AllBookmarksFragment :
if (selectionController?.onItemClick(item.pageId) != true) {
val intent = ReaderIntent.Builder(view.context)
.bookmark(item)
.incognito(true)
.incognito()
.build()
router.openReader(intent)
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()

View File

@@ -27,8 +27,8 @@ value class ReaderIntent private constructor(
intent.putExtra(AppRouter.KEY_ID, mangaId)
}
fun incognito(incognito: Boolean) = apply {
intent.putExtra(EXTRA_INCOGNITO, incognito)
fun incognito() = apply {
intent.putExtra(EXTRA_INCOGNITO, true)
}
fun branch(branch: String?) = apply {

View File

@@ -214,8 +214,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val progressIndicatorMode: ProgressIndicatorMode
get() = prefs.getEnumValue(KEY_PROGRESS_INDICATORS, ProgressIndicatorMode.PERCENT_READ)
val isHistoryExcludeNsfw: Boolean
get() = prefs.getBoolean(KEY_HISTORY_EXCLUDE_NSFW, false)
var incognitoModeForNsfw: TriStateOption
get() = prefs.getEnumValue(KEY_INCOGNITO_NSFW, TriStateOption.ASK)
set(value) = prefs.edit { putEnumValue(KEY_INCOGNITO_NSFW, value) }
var isIncognitoModeEnabled: Boolean
get() = prefs.getBoolean(KEY_INCOGNITO_MODE, false)
@@ -545,6 +546,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
prefs.edit { putStringSet(KEY_TIPS_CLOSED, closedTips + tip) }
}
fun isIncognitoModeEnabled(isNsfw: Boolean): Boolean {
return isIncognitoModeEnabled || (isNsfw && incognitoModeForNsfw == TriStateOption.ENABLED)
}
fun getPagesSaveDir(context: Context): DocumentFile? =
prefs.getString(KEY_PAGES_SAVE_DIR, null)?.toUriOrNull()?.let {
DocumentFile.fromTreeUri(context, it)?.takeIf { it.canWrite() }
@@ -656,7 +661,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PROGRESS_INDICATORS = "progress_indicators"
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"
const val KEY_INCOGNITO_NSFW = "incognito_nsfw"
const val KEY_PAGES_NUMBERS = "pages_numbers"
const val KEY_SCREENSHOTS_POLICY = "screenshots_policy"
const val KEY_PAGES_PRELOAD = "pages_preload"

View File

@@ -3,10 +3,13 @@ package org.koitharu.kotatsu.details.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.TriStateOption
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
@@ -53,14 +56,15 @@ class DetailsInteractor @Inject constructor(
}
}
fun observeIncognitoMode(mangaFlow: Flow<Manga?>): Flow<Boolean> {
fun observeIncognitoMode(mangaFlow: Flow<Manga?>): Flow<TriStateOption> {
return mangaFlow
.distinctUntilChangedBy { it?.isNsfw }
.flatMapLatest { manga ->
if (manga != null) {
historyRepository.observeShouldSkip(manga)
} else {
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }
.filterNotNull()
.distinctUntilChangedBy { it.isNsfw() }
.combine(observeIncognitoMode()) { manga, globalIncognito ->
when {
globalIncognito -> TriStateOption.ENABLED
manga.isNsfw() -> settings.incognitoModeForNsfw
else -> TriStateOption.DISABLED
}
}
}
@@ -87,4 +91,8 @@ class DetailsInteractor @Inject constructor(
}
suspend fun findRemote(seed: Manga) = localMangaRepository.getRemoteManga(seed)
private fun observeIncognitoMode() = settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) {
isIncognitoModeEnabled
}
}

View File

@@ -263,7 +263,7 @@ class DetailsActivity :
}
override fun onItemClick(item: Bookmark, view: View) {
router.openReader(ReaderIntent.Builder(view.context).bookmark(item).incognito(true).build())
router.openReader(ReaderIntent.Builder(view.context).bookmark(item).incognito().build())
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
}

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.nav.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.TriStateOption
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.computeSize
@@ -63,7 +64,7 @@ class DetailsViewModel @Inject constructor(
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
@LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
downloadScheduler: DownloadWorker.Scheduler,
private val interactor: DetailsInteractor,
interactor: DetailsInteractor,
savedStateHandle: SavedStateHandle,
deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
private val relatedMangaUseCase: RelatedMangaUseCase,
@@ -113,7 +114,7 @@ class DetailsViewModel @Inject constructor(
interactor.observeIncognitoMode(manga),
) { m, b, h, im ->
val estimatedTime = readingTimeUseCase.invoke(m, b, h)
HistoryInfo(m, b, h, im, estimatedTime)
HistoryInfo(m, b, h, im == TriStateOption.ENABLED, estimatedTime)
}.withErrorHandling()
.stateIn(
scope = viewModelScope + Dispatchers.Default,

View File

@@ -111,13 +111,13 @@ class ReadButtonDelegate(
Snackbar.make(buttonRead, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
.show() // TODO
} else {
router.openReader(
ReaderIntent.Builder(context)
.manga(manga)
.branch(viewModel.selectedBranchValue)
.incognito(isIncognitoMode)
.build(),
)
val intentBuilder = ReaderIntent.Builder(context)
.manga(manga)
.branch(viewModel.selectedBranchValue)
if (isIncognitoMode) {
intentBuilder.incognito()
}
router.openReader(intentBuilder.build())
if (isIncognitoMode) {
Toast.makeText(context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
}

View File

@@ -49,7 +49,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderViewModel
abstract class ChaptersPagesViewModel(
@JvmField protected val settings: AppSettings,
private val interactor: DetailsInteractor,
@JvmField protected val interactor: DetailsInteractor,
private val bookmarksRepository: BookmarksRepository,
private val historyRepository: HistoryRepository,
private val downloadScheduler: DownloadWorker.Scheduler,

View File

@@ -140,7 +140,7 @@ class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
val intent = ReaderIntent.Builder(view.context)
.manga(activityViewModel.getMangaOrNull() ?: return)
.bookmark(item)
.incognito(true)
.incognito()
.build()
router.openReader(intent)
}

View File

@@ -288,7 +288,7 @@ class MangaSourcesRepository @Inject constructor(
}
suspend fun trackUsage(source: MangaSource) {
if (!settings.isIncognitoModeEnabled && !(settings.isHistoryExcludeNsfw && source.isNsfw())) {
if (!settings.isIncognitoModeEnabled(source.isNsfw())) {
dao.setLastUsed(source.name, System.currentTimeMillis())
}
}

View File

@@ -201,13 +201,11 @@ class HistoryRepository @Inject constructor(
return db.getHistoryDao().findPopularSources(limit).toMangaSources()
}
fun shouldSkip(manga: Manga): Boolean {
return ((manga.source.isNsfw() || manga.isNsfw) && settings.isHistoryExcludeNsfw) || settings.isIncognitoModeEnabled
}
fun shouldSkip(manga: Manga): Boolean = settings.isIncognitoModeEnabled(manga.isNsfw())
fun observeShouldSkip(manga: Manga): Flow<Boolean> {
return settings.observe()
.filter { key -> key == AppSettings.KEY_INCOGNITO_MODE || key == AppSettings.KEY_HISTORY_EXCLUDE_NSFW }
.filter { key -> key == AppSettings.KEY_INCOGNITO_MODE || key == AppSettings.KEY_INCOGNITO_NSFW }
.onStart { emit("") }
.map { shouldSkip(manga) }
.distinctUntilChanged()

View File

@@ -25,7 +25,7 @@ class HistoryListQuickFilter @Inject constructor(
add(ListFilterOption.Macro.COMPLETED)
add(ListFilterOption.Macro.FAVORITE)
add(ListFilterOption.NOT_FAVORITE)
if (!settings.isNsfwContentDisabled && !settings.isHistoryExcludeNsfw) {
if (!settings.isNsfwContentDisabled) {
add(ListFilterOption.Macro.NSFW)
}
repository.getPopularTags(3).mapTo(this) {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.reader.ui
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
@@ -35,6 +36,8 @@ import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.ui.BaseFullscreenActivity
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.ui.dialog.setCheckbox
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
import org.koitharu.kotatsu.core.util.IdlingDetector
@@ -145,6 +148,7 @@ class ReaderActivity :
viewModel.isInfoBarTransparent.observe(this) { viewBinding.infoBar.drawBackground = !it }
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
viewModel.isBookmarkAdded.observe(this, MenuInvalidator(this))
viewModel.onAskNsfwIncognito.observeEvent(this) { askForIncognitoMode() }
viewModel.onShowToast.observeEvent(this) { msgId ->
Snackbar.make(viewBinding.container, msgId, Snackbar.LENGTH_SHORT)
.setAnchorView(viewBinding.toolbarDocked)
@@ -432,6 +436,30 @@ class ReaderActivity :
viewBinding.actionsView.isPrevEnabled = uiState.hasPreviousChapter()
}
private fun askForIncognitoMode() {
buildAlertDialog(this, isCentered = true) {
var dontAskAgain = false
val listener = DialogInterface.OnClickListener { _, which ->
if (which == DialogInterface.BUTTON_NEUTRAL) {
finishAfterTransition()
} else {
viewModel.setIncognitoMode(which == DialogInterface.BUTTON_POSITIVE, dontAskAgain)
}
}
setCheckbox(R.string.dont_ask_again, dontAskAgain) { _, isChecked ->
dontAskAgain = isChecked
}
setIcon(R.drawable.ic_incognito)
setTitle(R.string.incognito_mode)
setMessage(R.string.incognito_mode_hint_nsfw)
setPositiveButton(R.string.incognito, listener)
setNegativeButton(R.string.disable, listener)
setNeutralButton(android.R.string.cancel, listener)
setOnCancelListener { finishAfterTransition() }
setCancelable(true)
}.show()
}
companion object {
private const val TOAST_DURATION = 2000L

View File

@@ -35,10 +35,12 @@ import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.TriStateOption
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.firstNotNull
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.DetailsInteractor
@@ -112,14 +114,10 @@ class ReaderViewModel @Inject constructor(
val readerMode = MutableStateFlow<ReaderMode?>(null)
val onPageSaved = MutableEventFlow<Collection<Uri>>()
val onShowToast = MutableEventFlow<Int>()
val onAskNsfwIncognito = MutableEventFlow<Unit>()
val uiState = MutableStateFlow<ReaderUiState?>(null)
val incognitoMode = if (savedStateHandle.get<Boolean>(ReaderIntent.EXTRA_INCOGNITO) == true) {
MutableStateFlow(true)
} else {
interactor.observeIncognitoMode(manga)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
}
val isIncognitoMode = MutableStateFlow(savedStateHandle.get<Boolean>(ReaderIntent.EXTRA_INCOGNITO))
val content = MutableStateFlow(ReaderContent(emptyList(), null))
@@ -191,10 +189,13 @@ class ReaderViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
init {
initIncognitoMode()
loadImpl()
launchJob(Dispatchers.Default) {
val mangaId = manga.filterNotNull().first().id
appShortcutManager.notifyMangaOpened(mangaId)
if (!isIncognitoMode.firstNotNull()) {
appShortcutManager.notifyMangaOpened(mangaId)
}
}
}
@@ -228,7 +229,7 @@ class ReaderViewModel @Inject constructor(
readingState.value = state
savedStateHandle[ReaderIntent.EXTRA_STATE] = state
}
if (incognitoMode.value) {
if (isIncognitoMode.value != false) {
return
}
val readerState = state ?: readingState.value ?: return
@@ -381,6 +382,13 @@ class ReaderViewModel @Inject constructor(
}
}
fun setIncognitoMode(value: Boolean, dontAskAgain: Boolean) {
isIncognitoMode.value = value
if (dontAskAgain) {
settings.incognitoModeForNsfw = if (value) TriStateOption.ENABLED else TriStateOption.DISABLED
}
}
private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default) {
val details = detailsLoadUseCase.invoke(intent, force = false).first { x -> x.isLoaded }
@@ -399,7 +407,7 @@ class ReaderViewModel @Inject constructor(
chaptersLoader.loadSingleChapter(requireNotNull(readingState.value).chapterId)
// save state
if (!incognitoMode.value) {
if (!isIncognitoMode.firstNotNull()) {
readingState.value?.let {
val percent = computePercent(it.chapterId, it.page)
historyUpdateUseCase.invoke(manga, it, percent)
@@ -444,10 +452,10 @@ class ReaderViewModel @Inject constructor(
totalPages = chaptersLoader.getPagesCount(chapter.id),
currentPage = state.page,
percent = computePercent(state.chapterId, state.page),
incognito = incognitoMode.value,
incognito = isIncognitoMode.value == true,
)
uiState.value = newState
if (!incognitoMode.value) {
if (isIncognitoMode.value == false) {
statsCollector.onStateChanged(m.id, state)
}
}
@@ -481,6 +489,26 @@ class ReaderViewModel @Inject constructor(
valueProducer = { isReaderZoomButtonsEnabled },
)
private fun initIncognitoMode() {
if (isIncognitoMode.value != null) {
return
}
launchJob(Dispatchers.Default) {
interactor.observeIncognitoMode(manga)
.collect {
when (it) {
TriStateOption.ENABLED -> isIncognitoMode.value = true
TriStateOption.ASK -> {
onAskNsfwIncognito.call(Unit)
return@collect
}
TriStateOption.DISABLED -> isIncognitoMode.value = false
}
}
}
}
private suspend fun getStateFromIntent(manga: Manga): ReaderState {
val history = historyRepository.getOne(manga)
val preselectedBranch = selectedBranch.value

View File

@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
import org.koitharu.kotatsu.core.prefs.SearchSuggestionType
import org.koitharu.kotatsu.core.prefs.TriStateOption
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.observe
@@ -58,6 +59,10 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
entryValues = ScreenshotsPolicy.entries.names()
setDefaultValueCompat(ScreenshotsPolicy.ALLOW.name)
}
findPreference<ListPreference>(AppSettings.KEY_INCOGNITO_NSFW)?.run {
entryValues = TriStateOption.entries.names()
setDefaultValueCompat(TriStateOption.ASK.name)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -137,4 +137,9 @@
<item>@string/favourites</item>
<item>@string/saved_manga</item>
</string-array>
<string-array name="incognito_nsfw_options" translatable="false">
<item>@string/enable</item>
<item>@string/ask_every_time</item>
<item>@string/disable</item>
</string-array>
</resources>

View File

@@ -832,4 +832,7 @@
<string name="pick_custom_file">Pick custom file</string>
<string name="change_cover">Change cover</string>
<string name="page_switch_timer">The page will switch every ~%d seconds</string>
<string name="dont_ask_again">Don\'t ask again</string>
<string name="incognito_mode_hint_nsfw">This manga may contain adult content. Do you want to use incognito mode?</string>
<string name="incognito_for_nsfw">Incognito mode for NSFW manga</string>
</resources>

View File

@@ -17,10 +17,11 @@
android:title="@string/screenshots_policy"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:key="history_exclude_nsfw"
android:summary="@string/exclude_nsfw_from_history_summary"
android:title="@string/exclude_nsfw_from_history" />
<ListPreference
android:entries="@array/incognito_nsfw_options"
android:key="incognito_nsfw"
android:title="@string/incognito_for_nsfw"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="true"