Add option to enable incognito mode per extension (#157)

* add per Extension Incognito Mode

* migrate incognito sources when extension is updated

* remove incognito sources when extension is uninstalled

* remove not used variable

* address change requests

address change requests

* Rebase and cleanup code

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit c283abefb03f79ce6652492db71cde410f828f78)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
This commit is contained in:
sdaqo 2025-01-12 04:38:05 +01:00 committed by Jobobby04
parent 853288f71b
commit 0ab7d18ad3
13 changed files with 147 additions and 21 deletions

View File

@ -13,9 +13,11 @@ import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleIncognito
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
@ -190,5 +192,7 @@ class DomainModule : InjektModule {
addFactory { DeleteExtensionRepo(get()) }
addFactory { ReplaceExtensionRepo(get()) }
addFactory { UpdateExtensionRepo(get(), get()) }
addFactory { ToggleIncognito(get()) }
addFactory { GetIncognitoState(get(), get(), get()) }
}
}

View File

@ -0,0 +1,35 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
class GetIncognitoState(
private val basePreferences: BasePreferences,
private val sourcePreferences: SourcePreferences,
private val extensionManager: ExtensionManager,
) {
fun await(sourceId: Long?): Boolean {
if (basePreferences.incognitoMode().get()) return true
if (sourceId == null) return false
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false
return extensionPackage in sourcePreferences.incognitoExtensions().get()
}
fun subscribe(sourceId: Long?): Flow<Boolean> {
if (sourceId == null) return basePreferences.incognitoMode().changes()
return combine(
basePreferences.incognitoMode().changes(),
sourcePreferences.incognitoExtensions().changes(),
extensionManager.getExtensionPackageAsFlow(sourceId),
) { incognito, incognitoExtensions, extensionPackage ->
incognito || (extensionPackage in incognitoExtensions)
}
.distinctUntilChanged()
}
}

View File

@ -0,0 +1,14 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
class ToggleIncognito(
private val preferences: SourcePreferences,
) {
fun await(extensions: String, enable: Boolean) {
preferences.incognitoExtensions().getAndSet {
if (enable) it.plus(extensions) else it.minus(extensions)
}
}
}

View File

@ -22,6 +22,8 @@ class SourcePreferences(
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
fun lastUsedSource() = preferenceStore.getLong(

View File

@ -35,8 +35,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -48,6 +50,7 @@ import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
@ -73,6 +76,7 @@ fun ExtensionDetailsScreen(
onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
onClickIncognito: (Boolean) -> Unit,
) {
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
@ -141,9 +145,11 @@ fun ExtensionDetailsScreen(
contentPadding = paddingValues,
extension = state.extension,
sources = state.sources,
incognitoMode = state.isIncognito,
onClickSourcePreferences = onClickSourcePreferences,
onClickUninstall = onClickUninstall,
onClickSource = onClickSource,
onClickIncognito = onClickIncognito,
)
}
}
@ -153,9 +159,11 @@ private fun ExtensionDetails(
contentPadding: PaddingValues,
extension: Extension.Installed,
sources: ImmutableList<ExtensionSourceItem>,
incognitoMode: Boolean,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
onClickIncognito: (Boolean) -> Unit,
) {
val context = LocalContext.current
var showNsfwWarning by remember { mutableStateOf(false) }
@ -179,6 +187,7 @@ private fun ExtensionDetails(
item {
DetailsHeader(
extension = extension,
extIncognitoMode = incognitoMode,
onClickUninstall = onClickUninstall,
onClickAppInfo = {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
@ -190,6 +199,7 @@ private fun ExtensionDetails(
onClickAgeRating = {
showNsfwWarning = true
},
onExtIncognitoChange = onClickIncognito,
)
}
@ -217,9 +227,11 @@ private fun ExtensionDetails(
@Composable
private fun DetailsHeader(
extension: Extension,
extIncognitoMode: Boolean,
onClickAgeRating: () -> Unit,
onClickUninstall: () -> Unit,
onClickAppInfo: (() -> Unit)?,
onExtIncognitoChange: (Boolean) -> Unit,
) {
val context = LocalContext.current
@ -227,9 +239,8 @@ private fun DetailsHeader(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = MaterialTheme.padding.medium)
.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium,
bottom = MaterialTheme.padding.small,
)
@ -321,12 +332,9 @@ private fun DetailsHeader(
}
Row(
modifier = Modifier.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
modifier = Modifier
.padding(horizontal = MaterialTheme.padding.medium)
.padding(top = MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
OutlinedButton(
@ -349,6 +357,24 @@ private fun DetailsHeader(
}
}
TextPreferenceWidget(
modifier = Modifier.padding(horizontal = MaterialTheme.padding.small),
title = stringResource(MR.strings.pref_incognito_mode),
subtitle = stringResource(MR.strings.pref_incognito_mode_extension_summary),
icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
widget = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Switch(
checked = extIncognitoMode,
onCheckedChange = onExtIncognitoChange,
modifier = Modifier.padding(start = TrailingWidgetBuffer),
)
}
},
)
HorizontalDivider()
}
}

View File

@ -89,12 +89,25 @@ class ExtensionManager(
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = installedExtensionMapFlow.value.values
.find { ext ->
ext.sources.any { it.id == sourceId }
}
fun getExtensionPackage(sourceId: Long): String? {
return installedExtensionsFlow.value.find { extension ->
extension.sources.any { it.id == sourceId }
}
?.pkgName
}
fun getExtensionPackageAsFlow(sourceId: Long): Flow<String?> {
return installedExtensionsFlow.map { extensions ->
extensions.find { extension ->
extension.sources.any { it.id == sourceId }
}
?.pkgName
}
}
fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = getExtensionPackage(sourceId) ?: return null
if (pkgName != null) {
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!

View File

@ -39,6 +39,7 @@ data class ExtensionDetailsScreen(
onClickClearCookies = screenModel::clearCookies,
onClickUninstall = screenModel::uninstallExtension,
onClickSource = screenModel::toggleSource,
onClickIncognito = screenModel::toggleIncognito,
)
LaunchedEffect(Unit) {

View File

@ -6,7 +6,9 @@ import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.source.interactor.ToggleIncognito
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.network.NetworkHelper
@ -19,6 +21,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
@ -36,6 +39,8 @@ class ExtensionDetailsScreenModel(
private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensionSources: GetExtensionSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleIncognito: ToggleIncognito = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(),
) : StateScreenModel<ExtensionDetailsScreenModel.State>(State()) {
private val _events: Channel<ExtensionDetailsEvent> = Channel()
@ -80,6 +85,15 @@ class ExtensionDetailsScreenModel(
}
}
}
launch {
preferences.incognitoExtensions()
.changes()
.map { pkgName in it }
.distinctUntilChanged()
.collectLatest { isIncognito ->
mutableState.update { it.copy(isIncognito = isIncognito) }
}
}
}
}
@ -118,9 +132,16 @@ class ExtensionDetailsScreenModel(
?.let { toggleSource.await(it, enable) }
}
fun toggleIncognito(enable: Boolean) {
state.value.extension?.pkgName?.let { packageName ->
toggleIncognito.await(packageName, enable)
}
}
@Immutable
data class State(
val extension: Extension.Installed? = null,
val isIncognito: Boolean = false,
private val _sources: ImmutableList<ExtensionSourceItem>? = null,
) {

View File

@ -75,7 +75,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
data class BrowseSourceScreen(
private val sourceId: Long,
val sourceId: Long,
private val listingQuery: String?,
// SY -->
private val filtersJson: String? = null,

View File

@ -15,10 +15,10 @@ import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource
import eu.kanade.core.preference.asState
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.ui.UiPreferences
@ -93,7 +93,6 @@ open class BrowseSourceScreenModel(
// SY <--
private val sourceManager: SourceManager = Injekt.get(),
sourcePreferences: SourcePreferences = Injekt.get(),
basePreferences: BasePreferences = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val getRemoteManga: GetRemoteManga = Injekt.get(),
@ -105,6 +104,7 @@ open class BrowseSourceScreenModel(
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(),
private val getIncognitoState: GetIncognitoState = Injekt.get(),
// SY -->
unsortedPreferences: UnsortedPreferences = Injekt.get(),
@ -149,7 +149,7 @@ open class BrowseSourceScreenModel(
}
}
if (!basePreferences.incognitoMode().get()) {
if (!getIncognitoState.await(source.id)) {
sourcePreferences.lastUsedSource().set(source.id)
}

View File

@ -54,6 +54,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
@ -122,6 +123,8 @@ class MainActivity : BaseActivity() {
private val downloadCache: DownloadCache by injectLazy()
private val chapterCache: ChapterCache by injectLazy()
private val getIncognitoState: GetIncognitoState by injectLazy()
// To be checked by splash screen. If true then splash screen will be removed.
var ready = false
@ -181,7 +184,7 @@ class MainActivity : BaseActivity() {
setComposeContent {
val context = LocalContext.current
val incognito by preferences.incognitoMode().collectAsState()
var incognito by remember { mutableStateOf(getIncognitoState.await(null)) }
val downloadOnly by preferences.downloadedOnly().collectAsState()
val indexing by downloadCache.isInitializing.collectAsState()
@ -231,6 +234,11 @@ class MainActivity : BaseActivity() {
// SY <--
}
}
LaunchedEffect(navigator.lastItem) {
(navigator.lastItem as? BrowseSourceScreen)?.sourceId
.let(getIncognitoState::subscribe)
.collectLatest { incognito = it }
}
val scaffoldInsets = WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
Scaffold(

View File

@ -14,6 +14,7 @@ import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.model.readerOrientation
import eu.kanade.domain.manga.model.readingMode
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.domain.track.service.TrackPreferences
@ -115,7 +116,6 @@ class ReaderViewModel @JvmOverloads constructor(
private val downloadProvider: DownloadProvider = Injekt.get(),
private val tempFileManager: UniFileTempFileManager = Injekt.get(),
private val imageSaver: ImageSaver = Injekt.get(),
preferences: BasePreferences = Injekt.get(),
val readerPreferences: ReaderPreferences = Injekt.get(),
private val basePreferences: BasePreferences = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
@ -127,8 +127,9 @@ class ReaderViewModel @JvmOverloads constructor(
private val upsertHistory: UpsertHistory = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(),
private val syncPreferences: SyncPreferences = Injekt.get(),
private val getIncognitoState: GetIncognitoState = Injekt.get(),
// SY -->
private val syncPreferences: SyncPreferences = Injekt.get(),
private val uiPreferences: UiPreferences = Injekt.get(),
private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(),
private val getMergedMangaById: GetMergedMangaById = Injekt.get(),
@ -264,7 +265,7 @@ class ReaderViewModel @JvmOverloads constructor(
.map(::ReaderChapter)
}
private val incognitoMode = preferences.incognitoMode().get()
private val incognitoMode: Boolean by lazy { getIncognitoState.await(manga?.source) }
private val downloadAheadAmount = downloadPreferences.autoDownloadWhileReading().get()
init {

View File

@ -637,6 +637,7 @@
<string name="label_downloaded_only">Downloaded only</string>
<string name="pref_incognito_mode">Incognito mode</string>
<string name="pref_incognito_mode_summary">Pauses reading history</string>
<string name="pref_incognito_mode_extension_summary">Pause reading history for extension</string>
<string name="notification_incognito_text">Disable incognito mode</string>
<string name="downloaded_only_summary">Filters all entries in your library</string>