feat(incognito): add animated incognito mode indicator and icon (#899)

* Add indicator for source incognito mode

Adds an indicator to the manga info header to show if a source is running in incognito mode.
Also moves the source incognito check logic to `SourceExtensions`.

(cherry picked from commit 5e547efda51cdeaa7bc71198b932c8ac9aecc312)

* Replaced incognito icon with animated glasses_with_hat icon.

This commit introduces an animated vector drawable for the incognito mode icon and updates the related icons in the app.

- Adds `anim_incognito.xml`, a new animated vector drawable for incognito mode.
- Adds `ic_glasses_with_hat_24dp.xml`, a new icon for incognito mode.
- Updates the incognito mode icon in the following places:
    - MoreScreen
    - ExtensionDetailsScreen
    - App notification
    - Manga info header
- Updates `SwitchPreferenceWidget` and `TextPreferenceWidget` to accept any type of icons.

(cherry picked from commit 2a28673e078ff15fe0a7c2a030ffc6ce6cbe6b31)

* move incognito icon

* issue: Use stringResource for contentDescription to support localization

* Wrap the ImageVector.vectorResource call in remember to avoid inflating the drawable on every recomposition.

---------

Co-authored-by: Cuong-Tran <cuongtran.tm@gmail.com>
This commit is contained in:
Jery 2025-05-11 02:37:15 +07:00 committed by Cuong-Tran
parent 4fcbd88618
commit 3d9dd9d7b1
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
10 changed files with 168 additions and 13 deletions

View file

@ -4,6 +4,9 @@ import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.util.DisplayMetrics
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -36,10 +39,8 @@ 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
@ -374,7 +375,12 @@ private fun DetailsHeader(
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),
// KMK -->
icon = rememberAnimatedVectorPainter(
AnimatedImageVector.animatedVectorResource(R.drawable.anim_incognito),
extIncognitoMode,
),
// KMK <--
widget = {
Row(
verticalAlignment = Alignment.CenterVertically,

View file

@ -88,6 +88,7 @@ import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.source.isIncognitoModeEnabled
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.source.online.all.MangaDex
@ -621,6 +622,9 @@ private fun MangaScreenSmallImpl(
manga = state.manga,
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
isStubSource = remember { state.source is StubSource },
// KMK -->
isSourceIncognito = remember { state.source.isIncognitoModeEnabled() },
// KMK <--
onCoverClick = onCoverClicked,
doSearch = onSearch,
// KMK -->
@ -1075,6 +1079,9 @@ private fun MangaScreenLargeImpl(
manga = state.manga,
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
isStubSource = remember { state.source is StubSource },
// KMK -->
isSourceIncognito = remember { state.source.isIncognitoModeEnabled() },
// KMK <--
onCoverClick = onCoverClicked,
doSearch = onSearch,
// KMK -->

View file

@ -67,9 +67,11 @@ import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
@ -115,6 +117,9 @@ fun MangaInfoBox(
manga: Manga,
sourceName: String,
isStubSource: Boolean,
// KMK -->
isSourceIncognito: Boolean,
// KMK <--
onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit,
modifier: Modifier = Modifier,
@ -177,6 +182,9 @@ fun MangaInfoBox(
manga = manga,
sourceName = sourceName,
isStubSource = isStubSource,
// KMK -->
isSourceIncognito = isSourceIncognito,
// KMK <--
onCoverClick = onCoverClick,
doSearch = doSearch,
// KMK -->
@ -194,6 +202,9 @@ fun MangaInfoBox(
manga = manga,
sourceName = sourceName,
isStubSource = isStubSource,
// KMK -->
isSourceIncognito = isSourceIncognito,
// KMK <--
onCoverClick = onCoverClick,
doSearch = doSearch,
// KMK -->
@ -469,6 +480,9 @@ private fun MangaAndSourceTitlesLarge(
manga: Manga,
sourceName: String,
isStubSource: Boolean,
// KMK -->
isSourceIncognito: Boolean,
// KMK <--
onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit,
// KMK -->
@ -530,6 +544,9 @@ private fun MangaAndSourceTitlesLarge(
status = manga.status,
sourceName = sourceName,
isStubSource = isStubSource,
// KMK -->
isSourceIncognito = isSourceIncognito,
// KMK <--
doSearch = doSearch,
textAlign = TextAlign.Center,
// KMK -->
@ -546,6 +563,9 @@ private fun MangaAndSourceTitlesSmall(
manga: Manga,
sourceName: String,
isStubSource: Boolean,
// KMK -->
isSourceIncognito: Boolean,
// KMK <--
onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit,
// KMK -->
@ -619,6 +639,9 @@ private fun MangaAndSourceTitlesSmall(
status = manga.status,
sourceName = sourceName,
isStubSource = isStubSource,
// KMK -->
isSourceIncognito = isSourceIncognito,
// KMK <--
doSearch = doSearch,
// KMK -->
librarySearch = librarySearch,
@ -638,6 +661,9 @@ private fun ColumnScope.MangaContentInfo(
status: Long,
sourceName: String,
isStubSource: Boolean,
// KMK -->
isSourceIncognito: Boolean,
// KMK <--
doSearch: (query: String, global: Boolean) -> Unit,
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
// KMK -->
@ -792,6 +818,19 @@ private fun ColumnScope.MangaContentInfo(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
// KMK -->
if (isSourceIncognito) {
DotSeparatorText()
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(R.drawable.ic_glasses_with_hat_24dp)),
contentDescription = stringResource(MR.strings.pref_incognito_mode),
modifier = Modifier
.padding(end = 4.dp)
.size(16.dp),
tint = MaterialTheme.colorScheme.primary,
)
}
// KMK <--
DotSeparatorText()
if (isStubSource) {
Icon(

View file

@ -1,5 +1,8 @@
package eu.kanade.presentation.more
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label
@ -15,9 +18,7 @@ import androidx.compose.material.icons.outlined.Storage
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.R
@ -86,7 +87,12 @@ fun MoreScreen(
SwitchPreferenceWidget(
title = stringResource(MR.strings.pref_incognito_mode),
subtitle = stringResource(MR.strings.pref_incognito_mode_summary),
icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
// KMK -->
icon = rememberAnimatedVectorPainter(
AnimatedImageVector.animatedVectorResource(R.drawable.anim_incognito),
incognitoMode,
),
// KMK <--
checked = incognitoMode,
onCheckedChanged = onIncognitoModeChange,
)

View file

@ -8,6 +8,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
@ -17,7 +18,10 @@ fun SwitchPreferenceWidget(
modifier: Modifier = Modifier,
title: String,
subtitle: CharSequence? = null,
icon: ImageVector? = null,
// KMK -->
/** Can be either [ImageVector] or [Painter] */
icon: Any? = null,
// KMK <--
checked: Boolean = false,
onCheckedChanged: (Boolean) -> Unit,
) {

View file

@ -11,6 +11,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
@ -25,7 +26,10 @@ fun TextPreferenceWidget(
modifier: Modifier = Modifier,
title: String? = null,
subtitle: CharSequence? = null,
icon: ImageVector? = null,
// KMK -->
/** Can be either [ImageVector] or [Painter] */
icon: Any? = null,
// KMK <--
iconTint: Color = MaterialTheme.colorScheme.primary,
widget: @Composable (() -> Unit)? = null,
onPreferenceClick: (() -> Unit)? = null,
@ -60,16 +64,22 @@ fun TextPreferenceWidget(
} else {
null
},
icon = if (icon != null) {
{
icon = {
if (icon != null && icon is ImageVector) {
Icon(
imageVector = icon,
tint = iconTint,
contentDescription = null,
)
// KMK -->
} else if (icon != null && icon is Painter) {
Icon(
painter = icon,
tint = iconTint,
contentDescription = null,
)
// KMK <--
}
} else {
null
},
onClick = onPreferenceClick,
widget = widget,

View file

@ -164,7 +164,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
) {
setContentTitle(stringResource(MR.strings.pref_incognito_mode))
setContentText(stringResource(MR.strings.notification_incognito_text))
setSmallIcon(R.drawable.ic_glasses_24dp)
setSmallIcon(R.drawable.ic_glasses_with_hat_24dp)
setColor(ContextCompat.getColor(applicationContext, R.color.ic_launcher))
setLargeIcon(BitmapFactory.decodeResource(applicationContext.resources, R.drawable.komikku))
setOngoing(true)

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.source
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import tachiyomi.domain.source.model.StubSource
import tachiyomi.presentation.core.icons.FlagEmoji
import tachiyomi.source.local.isLocal
@ -77,3 +78,10 @@ private fun getMergedSourcesString(
// SY <--
fun Source.isLocalOrStub(): Boolean = isLocal() || this is StubSource
// KMK -->
fun Source.isIncognitoModeEnabled(): Boolean {
val extensionPackage = Injekt.get<ExtensionManager>().getExtensionPackage(id)
return extensionPackage in Injekt.get<SourcePreferences>().incognitoExtensions().get()
}
// KMK <--

View file

@ -0,0 +1,52 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:name="hat"
android:pivotX="12"
android:pivotY="12"
android:scaleX="1.2"
android:scaleY="1.2">
<path
android:fillColor="#000"
android:pathData="M15.53 2.63C15.31 2.14 14.75 1.88 14.22 2.05L12 2.79L9.77 2.05L9.72 2.04C9.19 1.89 8.63 2.17 8.43 2.68L6 9H18L15.56 2.68L15.53 2.63Z M22 10.5H2V12H22V10.5Z M0 0L0 0Z" />
</group>
<group
android:name="glasses"
android:translateY="5">
<path
android:fillColor="#000"
android:pathData="M3,10C2.76,10 2.55,10.09 2.41,10.25C2.27,10.4 2.21,10.62 2.24,10.86L2.74,13.85C2.82,14.5 3.4,15 4,15H7C7.64,15 8.36,14.44 8.5,13.82L9.56,10.63C9.6,10.5 9.57,10.31 9.5,10.19C9.39,10.07 9.22,10 9,10H3M7,17H4C2.38,17 0.96,15.74 0.76,14.14L0.26,11.15C0.15,10.3 0.39,9.5 0.91,8.92C1.43,8.34 2.19,8 3,8H9C9.83,8 10.58,8.35 11.06,8.96C11.17,9.11 11.27,9.27 11.35,9.45C11.78,9.36 12.22,9.36 12.64,9.45C12.72,9.27 12.82,9.11 12.94,8.96C13.41,8.35 14.16,8 15,8H21C21.81,8 22.57,8.34 23.09,8.92C23.6,9.5 23.84,10.3 23.74,11.11L23.23,14.18C23.04,15.74 21.61,17 20,17H17C15.44,17 13.92,15.81 13.54,14.3L12.64,11.59C12.26,11.31 11.73,11.31 11.35,11.59L10.43,14.37C10.07,15.82 8.56,17 7,17M15,10C14.78,10 14.61,10.07 14.5,10.19C14.42,10.31 14.4,10.5 14.45,10.7L15.46,13.75C15.64,14.44 16.36,15 17,15H20C20.59,15 21.18,14.5 21.25,13.89L21.76,10.82C21.79,10.62 21.73,10.4 21.59,10.25C21.45,10.09 21.24,10 21,10H15Z" />
</group>
</vector>
</aapt:attr>
<target android:name="hat">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleY"
android:valueFrom="0"
android:valueTo="1.2"
android:valueType="floatType" />
</aapt:attr>
</target>
<target android:name="glasses">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="translateY"
android:valueFrom="0"
android:valueTo="5"
android:valueType="floatType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:name="hat"
android:pivotX="12"
android:pivotY="10"
android:scaleX="1.2"
android:scaleY="1.2">
<path
android:fillColor="#FFF"
android:pathData="M15.53 2.63C15.31 2.14 14.75 1.88 14.22 2.05L12 2.79L9.77 2.05L9.72 2.04C9.19 1.89 8.63 2.17 8.43 2.68L6 9H18L15.56 2.68L15.53 2.63Z M22 10.5H2V12H22V10.5Z M0 0L0 0Z" />
</group>
<group
android:name="glasses"
android:translateY="5">
<path
android:fillColor="#FFF"
android:pathData="M3,10C2.76,10 2.55,10.09 2.41,10.25C2.27,10.4 2.21,10.62 2.24,10.86L2.74,13.85C2.82,14.5 3.4,15 4,15H7C7.64,15 8.36,14.44 8.5,13.82L9.56,10.63C9.6,10.5 9.57,10.31 9.5,10.19C9.39,10.07 9.22,10 9,10H3M7,17H4C2.38,17 0.96,15.74 0.76,14.14L0.26,11.15C0.15,10.3 0.39,9.5 0.91,8.92C1.43,8.34 2.19,8 3,8H9C9.83,8 10.58,8.35 11.06,8.96C11.17,9.11 11.27,9.27 11.35,9.45C11.78,9.36 12.22,9.36 12.64,9.45C12.72,9.27 12.82,9.11 12.94,8.96C13.41,8.35 14.16,8 15,8H21C21.81,8 22.57,8.34 23.09,8.92C23.6,9.5 23.84,10.3 23.74,11.11L23.23,14.18C23.04,15.74 21.61,17 20,17H17C15.44,17 13.92,15.81 13.54,14.3L12.64,11.59C12.26,11.31 11.73,11.31 11.35,11.59L10.43,14.37C10.07,15.82 8.56,17 7,17M15,10C14.78,10 14.61,10.07 14.5,10.19C14.42,10.31 14.4,10.5 14.45,10.7L15.46,13.75C15.64,14.44 16.36,15 17,15H20C20.59,15 21.18,14.5 21.25,13.89L21.76,10.82C21.79,10.62 21.73,10.4 21.59,10.25C21.45,10.09 21.24,10 21,10H15Z" />
</group>
</vector>