Repository: komikku-app/komikku
Pull request #899: feat(incognito): add animated incognito mode indicator and icon
Author: login: cuong-tran
Created: 2025-05-10T18:19:15Z | Merged: 2025-05-10T19:37:15Z

Introduce an animated icon for incognito mode and add an indicator in the manga info header to show if a source is in incognito mode. Update relevant screens and widgets to use the new icon and logic for incognito mode.

## Summary by Sourcery

Introduce an animated incognito mode icon and indicator across the app, including manga info headers and settings, and update UI components to support animated icons.

New Features:
- Add animated incognito mode icon and indicator to relevant screens and widgets.
- Display incognito mode status in the manga info header for sources.

Enhancements:
- Update preference widgets to support both static and animated icons.
- Replace static incognito icon with animated version in settings and extension details screens.
- Add new vector and animated vector drawable resources for incognito mode.


Diff:
diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt
index e257834e8a..2f6bc88658 100644
--- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt
@@ -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,
diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
index 0f453148d1..ddd6e5b061 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
@@ -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 -->
diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt
index 4b7b0e7eb1..34a86db69a 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt
@@ -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(
diff --git a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
index 704b69a6fc..830563a2f6 100644
--- a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
@@ -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,
                 )
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt
index 598669c58c..315c8c1490 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt
@@ -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,
 ) {
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt
index 1319decf69..f4ccb22d8f 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt
@@ -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,
diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt
index 8a0be59ada..89d86cb120 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/App.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt
@@ -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)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt
index a3a0124981..49c5f42b4a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt
@@ -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 <--
diff --git a/app/src/main/res/drawable/anim_incognito.xml b/app/src/main/res/drawable/anim_incognito.xml
new file mode 100644
index 0000000000..992a207f5c
--- /dev/null
+++ b/app/src/main/res/drawable/anim_incognito.xml
@@ -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>
diff --git a/app/src/main/res/drawable/ic_glasses_with_hat_24dp.xml b/app/src/main/res/drawable/ic_glasses_with_hat_24dp.xml
new file mode 100644
index 0000000000..42a066ac07
--- /dev/null
+++ b/app/src/main/res/drawable/ic_glasses_with_hat_24dp.xml
@@ -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>


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @cuong-tran, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request introduces an animated icon for incognito mode and adds an indicator in the manga info header to show if a source is in incognito mode. It updates relevant screens and widgets to use the new icon and logic for incognito mode. Specifically, it adds an animated vector drawable for the incognito icon, and modifies the MangaScreen, MangaInfoHeader, MoreScreen, ExtensionDetailsScreen, and related widgets to display the new icon and reflect the incognito mode status of sources.

### Highlights
* **Animated Incognito Icon**: Adds an animated vector drawable (`anim_incognito.xml`) to provide a visual cue for incognito mode. The animation involves the hat scaling and the glasses translating into view.
* **Incognito Mode Indicator**: Introduces an indicator in the manga info header to display whether a source is in incognito mode, using a glasses-with-hat icon.
* **UI Updates**: Updates the MoreScreen and ExtensionDetailsScreen to use the animated incognito icon in the settings.
* **Source Extension**: Adds a function `isIncognitoModeEnabled()` to the `Source` interface to determine if a source is in incognito mode based on the `incognitoExtensions` preferences.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt**
  * Imported necessary compose animation graphics libraries.
  * Replaced the static incognito icon with the new animated incognito icon using `rememberAnimatedVectorPainter`.
* **app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt**
  * Imported `isIncognitoModeEnabled` from source extensions.
  * Passed `isSourceIncognito` to `MangaScreenSmallImpl` and `MangaScreenLargeImpl` to indicate if the source is in incognito mode.
* **app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt**
  * Added `isSourceIncognito` parameter to `MangaInfoBox` and `MangaAndSourceTitlesLarge/Small` functions.
  * Added an incognito mode indicator (glasses with hat icon) in the `MangaContentInfo` composable when `isSourceIncognito` is true.
* **app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt**
  * Replaced the static incognito icon with the new animated incognito icon in the MoreScreen settings.
* **app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt**
  * Modified the `icon` parameter to accept either `ImageVector` or `Painter`.
* **app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt**
  * Modified the `icon` parameter to accept either `ImageVector` or `Painter` and updated the icon rendering logic.
* **app/src/main/java/eu/kanade/tachiyomi/App.kt**
  * Changed the small icon for the incognito mode notification to `ic_glasses_with_hat_24dp`.
* **app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt**
  * Added `isIncognitoModeEnabled()` extension function to the `Source` interface to check if the source is enabled for incognito mode.
* **app/src/main/res/drawable/anim_incognito.xml**
  * Added a new animated vector drawable for the incognito icon.
* **app/src/main/res/drawable/ic_glasses_with_hat_24dp.xml**
  * Added a new vector drawable for the incognito icon with glasses and a hat.

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> A masked icon appears,
> Privacy's gentle promise,
> Safe browsing assured.


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T18:19:29Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request effectively introduces an animated incognito mode icon and a visual indicator for sources in incognito mode. The changes are well-integrated into the existing UI components, and the new animated icon is a nice touch. The logic for determining incognito status and updating relevant UI elements appears sound. The generalization of icon types in preference widgets is also a good improvement for flexibility.

### Summary of Findings
* **Developer-Specific Comments**: The codebase includes several instances of `// KMK -->` and `// KMK <--` comments. These appear to be developer-specific markers and should be removed before merging to maintain code cleanliness.

### Merge Readiness
The pull request is in good shape overall and implements the described features effectively. However, there are several developer-specific comments (`// KMK -->` and `// KMK <--`) scattered throughout the changes. It's recommended to remove these comments to improve code maintainability before merging. Once these minor cleanups are addressed, the PR should be ready for merging. As a language model, I am not authorized to approve pull requests; please ensure further review and approval by authorized team members.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T18:21:24Z
user: login: sourcery-ai[bot]
body: Hey @cuong-tran - I've reviewed your changes and they look great!

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 5 issues found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T18:22:02Z
user: login: sourcery-ai[bot]
body: Hey @cuong-tran - I've reviewed your changes and they look great!

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 1 issue found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T19:26:36Z

Review comments:
diff_hunk: @@ -792,6 +817,21 @@ private fun ColumnScope.MangaContentInfo(
                 overflow = TextOverflow.Ellipsis,
                 maxLines = 1,
             )
+            // KMK -->
+            if (isSourceIncognito) {
+                DotSeparatorText()
+                Icon(
+                    // KMK -->
+                    imageVector = ImageVector.vectorResource(R.drawable.ic_glasses_with_hat_24dp),
path: app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt
user: login: sourcery-ai[bot]
body: **issue (performance):** Cache the ImageVector with remember to avoid reloads on every recomposition

Wrap the ImageVector.vectorResource call in remember (or hoist it) to avoid inflating the drawable on every recomposition.
created_at: 2025-05-10T18:22:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -792,6 +817,21 @@ private fun ColumnScope.MangaContentInfo(
                 overflow = TextOverflow.Ellipsis,
                 maxLines = 1,
             )
+            // KMK -->
+            if (isSourceIncognito) {
+                DotSeparatorText()
+                Icon(
+                    // KMK -->
+                    imageVector = ImageVector.vectorResource(R.drawable.ic_glasses_with_hat_24dp),
+                    // KMK <--
+                    contentDescription = "Incognito Mode State for Source",
path: app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt
user: login: sourcery-ai[bot]
body: **issue:** Use stringResource for contentDescription to support localization
created_at: 2025-05-10T18:22:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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,
path: app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt
user: login: sourcery-ai[bot]
body: **issue:** Avoid using Any? for icon parameter; reduce type ambiguity

Use a sealed class or separate overloads/parameters (ImageVector? and Painter?) instead of Any? to enforce type safety.
created_at: 2025-05-10T18:22:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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,
path: app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt
user: login: sourcery-ai[bot]
body: **issue:** Avoid using Any? for icon parameter; prefer explicit types

Consider using a typed parameter or sealed wrapper (as in TextPreferenceWidget) to improve clarity and safety.
created_at: 2025-05-10T18:22:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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() },
path: app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
user: login: sourcery-ai[bot]
body: **issue (bug_risk):** remember prevents updates when setting changes

Use derivedStateOf or a state holder instead of remember to track incognito mode toggles at runtime.
created_at: 2025-05-10T18:22:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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] */
path: app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Using Any? for icon type reduces type safety.

This change allows flexibility but sacrifices compile-time type checking, risking runtime errors if an unsupported type is passed. Use a sealed class or wrapper to explicitly support only ImageVector and Painter.
created_at: 2025-05-10T19:26:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-10T18:19:20Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This pull request introduces an animated incognito mode icon and updates the UI to display an incognito indicator in the manga info header when a source is in incognito mode. It also refactors preference widgets and settings screens to support animated and static incognito icons, adds logic to determine if a source is in incognito mode, and updates the notification icon accordingly.

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Add animated incognito icon and indicator in manga info header for incognito sources. | <ul><li>Pass isSourceIncognito flag through manga info header components</li><li>Display incognito indicator icon in manga info header when source is incognito</li></ul> | `app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt`<br/>`app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt` |
| Support animated vector icons in preference widgets and settings screens. | <ul><li>Update icon parameter to accept ImageVector or Painter</li><li>Render animated incognito icon in settings and extension details screens</li></ul> | `app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt`<br/>`app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt`<br/>`app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt`<br/>`app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt` |
| Add logic to determine if a source is in incognito mode. | <ul><li>Implement isIncognitoModeEnabled extension for Source</li><li>Use this logic in manga info header and related screens</li></ul> | `app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt`<br/>`app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt` |
| Update notification and UI to use new incognito icon with hat. | <ul><li>Change notification small icon to new incognito icon</li></ul> | `app/src/main/java/eu/kanade/tachiyomi/App.kt` |
| Add new animated and static incognito icon resources. | <ul><li>Add anim_incognito.xml for animated icon</li><li>Add ic_glasses_with_hat_24dp.xml for static icon</li></ul> | `app/src/main/res/drawable/anim_incognito.xml`<br/>`app/src/main/res/drawable/ic_glasses_with_hat_24dp.xml` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: cuong-tran
created_at: 2025-05-10T19:18:55Z
body: @sourcery-ai review
author_association: COLLABORATOR