Repository: komikku-app/komikku
Pull request #915: feat(updates-screen): Swipe actions for chapter updates
Author: login: cuong-tran
Created: 2025-05-16T03:27:11Z | Merged: 2025-05-16T07:05:49Z

## Summary by Sourcery

Add configurable swipe actions to chapter items in the Updates screen and manga chapter list based on user preferences, wiring gestures to toggle read/bookmark states and manage downloads.

New Features:
- Enable start/end swipe actions on chapter items in the Updates screen to trigger mark-read, bookmark toggle, or download operations.
- Enable swipe actions on manga chapter list items with configurable left and right swipe behaviors.
- Implement swipe handling in UpdatesScreenModel to perform toggle-read, toggle-bookmark, and download/cancel/delete actions based on user preference.

Enhancements:
- Extract getSwipeAction helper and unify swipeThreshold across components.
- Pass swipe action preferences from library settings through UpdatesScreenModel to composables.
- Add debug toggle to optionally hide cover images and render placeholder colors.
- Adjust UI dimensions for cover widths and action indicator sizes.


Diff:
diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt
index 32e88debc0..9b69517b58 100644
--- a/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt
+++ b/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt
@@ -27,10 +27,12 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
 import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.unit.dp
 import eu.kanade.presentation.manga.components.MangaCover
+import eu.kanade.presentation.manga.components.MangaCoverHide
 import eu.kanade.presentation.manga.components.RatioSwitchToPanorama
 import eu.kanade.presentation.theme.TachiyomiPreviewTheme
 import eu.kanade.presentation.util.formatChapterNumber
 import eu.kanade.tachiyomi.util.lang.toTimestampString
+import exh.debug.DebugToggles
 import tachiyomi.domain.history.model.HistoryWithRelations
 import tachiyomi.i18n.MR
 import tachiyomi.presentation.core.components.material.padding
@@ -63,37 +65,46 @@ fun HistoryItem(
         val coverIsWide = coverRatio.floatValue <= RatioSwitchToPanorama
         val bgColor = mangaCover.dominantCoverColors?.first?.let { Color(it) }
         val onBgColor = mangaCover.dominantCoverColors?.second
-        if (usePanoramaCover && coverIsWide) {
-            MangaCover.Panorama(
+        if (DebugToggles.HIDE_COVER_IMAGE_ONLY_SHOW_COLOR.enabled) {
+            MangaCoverHide.Book(
                 modifier = Modifier.fillMaxHeight(),
-                data = mangaCover,
-                onClick = onClickCover,
-                // KMK -->
                 bgColor = bgColor,
                 tint = onBgColor,
                 size = MangaCover.Size.Medium,
-                onCoverLoaded = { _, result ->
-                    val image = result.result.image
-                    coverRatio.floatValue = image.height.toFloat() / image.width
-                },
-                // KMK <--
             )
         } else {
-            // KMK <--
-            MangaCover.Book(
-                modifier = Modifier.fillMaxHeight(),
-                data = mangaCover,
-                onClick = onClickCover,
-                // KMK -->
-                bgColor = bgColor,
-                tint = onBgColor,
-                size = MangaCover.Size.Medium,
-                onCoverLoaded = { _, result ->
-                    val image = result.result.image
-                    coverRatio.floatValue = image.height.toFloat() / image.width
-                },
+            if (usePanoramaCover && coverIsWide) {
+                MangaCover.Panorama(
+                    modifier = Modifier.fillMaxHeight(),
+                    data = mangaCover,
+                    onClick = onClickCover,
+                    // KMK -->
+                    bgColor = bgColor,
+                    tint = onBgColor,
+                    size = MangaCover.Size.Medium,
+                    onCoverLoaded = { _, result ->
+                        val image = result.result.image
+                        coverRatio.floatValue = image.height.toFloat() / image.width
+                    },
+                    // KMK <--
+                )
+            } else {
                 // KMK <--
-            )
+                MangaCover.Book(
+                    modifier = Modifier.fillMaxHeight(),
+                    data = mangaCover,
+                    onClick = onClickCover,
+                    // KMK -->
+                    bgColor = bgColor,
+                    tint = onBgColor,
+                    size = MangaCover.Size.Medium,
+                    onCoverLoaded = { _, result ->
+                        val image = result.result.image
+                        coverRatio.floatValue = image.height.toFloat() / image.width
+                    },
+                    // KMK <--
+                )
+            }
         }
         Column(
             modifier = Modifier
diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt
index 9878ef3a4d..e06ec19081 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt
@@ -268,7 +268,7 @@ private fun Modifier.commonClickable(
     ),
 )
 
-private val IndicatorSize = 26.dp
+internal val IndicatorSize = 26.dp
 private val IndicatorPadding = 2.dp
 
 // To match composable parameter name when used later
diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt
index 55e61e5294..f89594d8b4 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Bookmark
@@ -68,27 +69,36 @@ fun MangaChapterListItem(
     onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
     modifier: Modifier = Modifier,
 ) {
-    val start = getSwipeAction(
-        action = chapterSwipeStartAction,
-        read = read,
-        bookmark = bookmark,
-        downloadState = downloadStateProvider(),
-        background = MaterialTheme.colorScheme.primaryContainer,
-        onSwipe = { onChapterSwipe(chapterSwipeStartAction) },
-    )
-    val end = getSwipeAction(
-        action = chapterSwipeEndAction,
-        read = read,
-        bookmark = bookmark,
-        downloadState = downloadStateProvider(),
-        background = MaterialTheme.colorScheme.primaryContainer,
-        onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
-    )
+    // KMK -->
+    val swipeBackground = MaterialTheme.colorScheme.primaryContainer
+    val swipeStart = remember(chapterSwipeStartAction, read, bookmark, downloadStateProvider()) {
+        // KMK <--
+        getSwipeAction(
+            action = chapterSwipeStartAction,
+            read = read,
+            bookmark = bookmark,
+            downloadState = downloadStateProvider(),
+            background = swipeBackground,
+            onSwipe = { onChapterSwipe(chapterSwipeStartAction) },
+        )
+    }
+    // KMK -->
+    val swipeEnd = remember(chapterSwipeEndAction, read, bookmark, downloadStateProvider()) {
+        // KMK <--
+        getSwipeAction(
+            action = chapterSwipeEndAction,
+            read = read,
+            bookmark = bookmark,
+            downloadState = downloadStateProvider(),
+            background = swipeBackground,
+            onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
+        )
+    }
 
     SwipeableActionsBox(
         modifier = Modifier.clipToBounds(),
-        startActions = listOfNotNull(start),
-        endActions = listOfNotNull(end),
+        startActions = listOfNotNull(swipeStart),
+        endActions = listOfNotNull(swipeEnd),
         swipeThreshold = swipeActionThreshold,
         backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
     ) {
@@ -200,7 +210,7 @@ fun MangaChapterListItem(
     }
 }
 
-private fun getSwipeAction(
+internal fun getSwipeAction(
     action: LibraryPreferences.ChapterSwipeAction,
     read: Boolean,
     bookmark: Boolean,
@@ -243,7 +253,9 @@ private fun swipeAction(
     return me.saket.swipe.SwipeAction(
         icon = {
             Icon(
-                modifier = Modifier.padding(16.dp),
+                modifier = Modifier
+                    .padding(horizontal = 16.dp)
+                    .size(IndicatorSize),
                 imageVector = icon,
                 tint = contentColorFor(background),
                 contentDescription = null,
@@ -255,4 +267,4 @@ private fun swipeAction(
     )
 }
 
-private val swipeActionThreshold = 56.dp
+internal val swipeActionThreshold = 56.dp
diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt
index 8ac2e16fed..287f4a849e 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt
@@ -30,6 +30,10 @@ import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.unit.dp
 import coil3.compose.AsyncImagePainter
 import coil3.compose.SubcomposeAsyncImage
+import eu.kanade.presentation.manga.components.MangaCover.Companion.COVER_TEMPLATE_SIZE_BIG
+import eu.kanade.presentation.manga.components.MangaCover.Companion.COVER_TEMPLATE_SIZE_MEDIUM
+import eu.kanade.presentation.manga.components.MangaCover.Companion.COVER_TEMPLATE_SIZE_NORMAL
+import eu.kanade.presentation.manga.components.MangaCover.Size
 import eu.kanade.tachiyomi.R
 import tachiyomi.domain.manga.model.Manga
 import tachiyomi.domain.manga.model.asMangaCover
@@ -175,6 +179,7 @@ enum class MangaCoverHide(private val ratio: Float) {
         bgColor: Color? = CoverPlaceholderColor,
         /** onBackground color, which used for loading/error indicator */
         @ColorInt tint: Int? = null,
+        size: Size = Size.Normal,
     ) {
         val modifierColored = modifier
             .aspectRatio(ratio)
@@ -198,7 +203,13 @@ enum class MangaCoverHide(private val ratio: Float) {
                 imageVector = ImageVector.vectorResource(R.drawable.ic_baseline_menu_book_24),
                 contentDescription = contentDescription,
                 modifier = Modifier
-                    .size(32.dp)
+                    .size(
+                        when (size) {
+                            Size.Big -> COVER_TEMPLATE_SIZE_BIG
+                            Size.Medium -> COVER_TEMPLATE_SIZE_MEDIUM
+                            else -> COVER_TEMPLATE_SIZE_NORMAL
+                        },
+                    )
                     .align(Alignment.Center),
                 colorFilter = ColorFilter.tint(
                     tint?.let { Color(it) } ?: CoverPlaceholderOnBgColor,
diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt
index 939052464c..3ab60d436e 100644
--- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt
@@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
+import tachiyomi.domain.library.service.LibraryPreferences
 import tachiyomi.i18n.MR
 import tachiyomi.i18n.kmk.KMR
 import tachiyomi.presentation.core.components.FastScrollLazyColumn
@@ -60,6 +61,11 @@ fun UpdateScreen(
     onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
     onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
     onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
+    // KMK -->
+    updateSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
+    updateSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
+    onUpdateSwipe: (UpdatesItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
+    // KMK <--
     onUpdateSelected: (UpdatesItem, /* KMK --> */ UpdatesScreenModel.UpdateSelectionOptions /* KMK <-- */) -> Unit,
     onOpenChapter: (UpdatesItem) -> Unit,
     // KMK -->
@@ -143,6 +149,11 @@ fun UpdateScreen(
                             onClickCover = onClickCover,
                             onClickUpdate = onOpenChapter,
                             onDownloadChapter = onDownloadChapter,
+                            // KMK -->
+                            updateSwipeStartAction = updateSwipeStartAction,
+                            updateSwipeEndAction = updateSwipeEndAction,
+                            onUpdateSwipe = onUpdateSwipe,
+                            // KMK <--
                         )
                     }
                 }
diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt
index 3c2d1845da..9e8033f303 100644
--- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt
+++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt
@@ -37,6 +37,7 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.platform.LocalDensity
@@ -48,7 +49,10 @@ import eu.kanade.presentation.manga.components.ChapterDownloadAction
 import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
 import eu.kanade.presentation.manga.components.DotSeparatorText
 import eu.kanade.presentation.manga.components.MangaCover
+import eu.kanade.presentation.manga.components.MangaCoverHide
 import eu.kanade.presentation.manga.components.RatioSwitchToPanorama
+import eu.kanade.presentation.manga.components.getSwipeAction
+import eu.kanade.presentation.manga.components.swipeActionThreshold
 import eu.kanade.presentation.util.animateItemFastScroll
 import eu.kanade.presentation.util.relativeTimeSpanString
 import eu.kanade.tachiyomi.R
@@ -56,7 +60,10 @@ import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.ui.updates.UpdatesItem
 import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel.UpdateSelectionOptions
 import eu.kanade.tachiyomi.ui.updates.groupByDateAndManga
+import exh.debug.DebugToggles
+import me.saket.swipe.SwipeableActionsBox
 import mihon.feature.upcoming.DateHeading
+import tachiyomi.domain.library.service.LibraryPreferences
 import tachiyomi.domain.updates.model.UpdatesWithRelations
 import tachiyomi.i18n.MR
 import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
@@ -96,6 +103,11 @@ internal fun LazyListScope.updatesUiItems(
     onClickCover: (UpdatesItem) -> Unit,
     onClickUpdate: (UpdatesItem) -> Unit,
     onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
+    // KMK -->
+    updateSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
+    updateSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
+    onUpdateSwipe: (UpdatesItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
+    // KMK <--
 ) {
     items(
         items = uiModels,
@@ -191,6 +203,11 @@ internal fun LazyListScope.updatesUiItems(
                         downloadStateProvider = updatesItem.downloadStateProvider,
                         downloadProgressProvider = updatesItem.downloadProgressProvider,
                         // KMK -->
+                        updateSwipeStartAction = updateSwipeStartAction,
+                        updateSwipeEndAction = updateSwipeEndAction,
+                        onUpdateSwipe = {
+                            onUpdateSwipe(updatesItem, it)
+                        },
                         isLeader = isLeader,
                         isExpandable = item.isExpandable,
                         expanded = isExpanded,
@@ -217,6 +234,9 @@ private fun UpdatesUiItem(
     downloadStateProvider: () -> Download.State,
     downloadProgressProvider: () -> Int,
     // KMK -->
+    updateSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
+    updateSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
+    onUpdateSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
     isLeader: Boolean,
     isExpandable: Boolean,
     expanded: Boolean,
@@ -231,152 +251,193 @@ private fun UpdatesUiItem(
     val haptic = LocalHapticFeedback.current
     val textAlpha = if (update.read) DISABLED_ALPHA else 1f
 
-    Row(
-        modifier = modifier
-            .selectedBackground(selected)
-            .combinedClickable(
-                onClick = onClick,
-                onLongClick = {
-                    onLongClick()
-                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)
-                },
-            )
-            .padding(
-                // KMK -->
-                vertical = if (isLeader) MaterialTheme.padding.extraSmall else 0.dp,
-                // KMK <--
-                horizontal = MaterialTheme.padding.medium,
-            ),
-        verticalAlignment = Alignment.CenterVertically,
+    // KMK -->
+    val swipeBackground = MaterialTheme.colorScheme.primaryContainer
+    val swipeStart = remember(updateSwipeStartAction, update.read, update.bookmark, downloadStateProvider()) {
+        getSwipeAction(
+            action = updateSwipeStartAction,
+            read = update.read,
+            bookmark = update.bookmark,
+            downloadState = downloadStateProvider(),
+            background = swipeBackground,
+            onSwipe = { onUpdateSwipe(updateSwipeStartAction) },
+        )
+    }
+    val swipeEnd = remember(updateSwipeEndAction, update.read, update.bookmark, downloadStateProvider()) {
+        getSwipeAction(
+            action = updateSwipeEndAction,
+            read = update.read,
+            bookmark = update.bookmark,
+            downloadState = downloadStateProvider(),
+            background = swipeBackground,
+            onSwipe = { onUpdateSwipe(updateSwipeEndAction) },
+        )
+    }
+
+    SwipeableActionsBox(
+        modifier = modifier.clipToBounds(),
+        startActions = listOfNotNull(swipeStart),
+        endActions = listOfNotNull(swipeEnd),
+        swipeThreshold = swipeActionThreshold,
+        backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
     ) {
-        // KMK -->
-        val mangaCover = update.coverData
-        val coverIsWide = coverRatio.floatValue <= RatioSwitchToPanorama
-        val bgColor = mangaCover.dominantCoverColors?.first?.let { Color(it) }
-        val onBgColor = mangaCover.dominantCoverColors?.second
-        if (isLeader) {
-            if (usePanoramaCover && coverIsWide) {
-                MangaCover.Panorama(
-                    modifier = Modifier
-                        .padding(top = MaterialTheme.padding.small)
-                        .width(UpdateItemPanoramaWidth),
-                    data = mangaCover,
-                    onClick = onClickCover,
-                    // KMK -->
-                    bgColor = bgColor,
-                    tint = onBgColor,
-                    size = MangaCover.Size.Medium,
-                    onCoverLoaded = { _, result ->
-                        val image = result.result.image
-                        coverRatio.floatValue = image.height.toFloat() / image.width
+        // KMK <--
+        Row(
+            modifier = Modifier
+                .selectedBackground(selected)
+                .combinedClickable(
+                    onClick = onClick,
+                    onLongClick = {
+                        onLongClick()
+                        haptic.performHapticFeedback(HapticFeedbackType.LongPress)
                     },
-                    // KMK <--
                 )
-            } else {
-                // KMK <--
-                MangaCover.Book(
-                    modifier = Modifier
-                        // KMK -->
-                        .padding(top = MaterialTheme.padding.small)
-                        .width(UpdateItemWidth),
-                    // KMK <--
-                    data = mangaCover,
-                    onClick = onClickCover,
+                .padding(top = if (isLeader) MaterialTheme.padding.small else 0.dp)
+                .padding(
                     // KMK -->
-                    bgColor = bgColor,
-                    tint = onBgColor,
-                    size = MangaCover.Size.Medium,
-                    onCoverLoaded = { _, result ->
-                        val image = result.result.image
-                        coverRatio.floatValue = image.height.toFloat() / image.width
-                    },
-                )
-            }
-        } else {
-            Box(
-                modifier = Modifier
-                    .width(if (usePanoramaCover && coverIsWide) UpdateItemPanoramaWidth else UpdateItemWidth),
-            )
-            // KMK <--
-        }
-
-        Column(
-            modifier = Modifier
-                .padding(horizontal = MaterialTheme.padding.medium)
-                .weight(1f),
+                    vertical = if (isLeader) MaterialTheme.padding.extraSmall else 0.dp,
+                    // KMK <--
+                    horizontal = MaterialTheme.padding.medium,
+                ),
+            verticalAlignment = Alignment.CenterVertically,
         ) {
             // KMK -->
+            val mangaCover = update.coverData
+            val coverIsWide = coverRatio.floatValue <= RatioSwitchToPanorama
+            val bgColor = mangaCover.dominantCoverColors?.first?.let { Color(it) }
+            val onBgColor = mangaCover.dominantCoverColors?.second
             if (isLeader) {
-                // KMK <--
-                Text(
-                    text = update.mangaTitle,
-                    maxLines = 1,
-                    style = MaterialTheme.typography.bodyMedium,
-                    color = LocalContentColor.current.copy(alpha = textAlpha),
-                    overflow = TextOverflow.Ellipsis,
-                )
-            }
-
-            Row(verticalAlignment = Alignment.CenterVertically) {
-                var textHeight by remember { mutableIntStateOf(0) }
-                if (!update.read) {
-                    Icon(
-                        imageVector = Icons.Filled.Circle,
-                        contentDescription = stringResource(MR.strings.unread),
+                if (DebugToggles.HIDE_COVER_IMAGE_ONLY_SHOW_COLOR.enabled) {
+                    MangaCoverHide.Book(
                         modifier = Modifier
-                            .height(8.dp)
-                            .padding(end = 4.dp),
-                        tint = MaterialTheme.colorScheme.primary,
+                            .width(UpdateItemWidth),
+                        bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { selected }),
+                        tint = onBgColor,
+                        size = MangaCover.Size.Medium,
                     )
+                } else {
+                    if (usePanoramaCover && coverIsWide) {
+                        MangaCover.Panorama(
+                            modifier = Modifier
+                                .width(UpdateItemPanoramaWidth),
+                            data = mangaCover,
+                            onClick = onClickCover,
+                            // KMK -->
+                            bgColor = bgColor,
+                            tint = onBgColor,
+                            size = MangaCover.Size.Medium,
+                            onCoverLoaded = { _, result ->
+                                val image = result.result.image
+                                coverRatio.floatValue = image.height.toFloat() / image.width
+                            },
+                            // KMK <--
+                        )
+                    } else {
+                        // KMK <--
+                        MangaCover.Book(
+                            modifier = Modifier
+                                // KMK -->
+                                .width(UpdateItemWidth),
+                            // KMK <--
+                            data = mangaCover,
+                            onClick = onClickCover,
+                            // KMK -->
+                            bgColor = bgColor,
+                            tint = onBgColor,
+                            size = MangaCover.Size.Medium,
+                            onCoverLoaded = { _, result ->
+                                val image = result.result.image
+                                coverRatio.floatValue = image.height.toFloat() / image.width
+                            },
+                        )
+                    }
                 }
-                if (update.bookmark) {
-                    Icon(
-                        imageVector = Icons.Filled.Bookmark,
-                        contentDescription = stringResource(MR.strings.action_filter_bookmarked),
-                        modifier = Modifier
-                            .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
-                        tint = MaterialTheme.colorScheme.primary,
-                    )
-                    Spacer(modifier = Modifier.width(2.dp))
-                }
-                Text(
-                    text = update.chapterName,
-                    maxLines = 1,
-                    style = MaterialTheme.typography.bodySmall,
-                    color = LocalContentColor.current.copy(alpha = textAlpha),
-                    overflow = TextOverflow.Ellipsis,
-                    onTextLayout = { textHeight = it.size.height },
+            } else {
+                Box(
                     modifier = Modifier
-                        .weight(weight = 1f, fill = false),
+                        .width(if (usePanoramaCover && coverIsWide) UpdateItemPanoramaWidth else UpdateItemWidth),
                 )
-                if (readProgress != null) {
-                    DotSeparatorText()
+                // KMK <--
+            }
+
+            Column(
+                modifier = Modifier
+                    .padding(horizontal = MaterialTheme.padding.medium)
+                    .weight(1f),
+            ) {
+                // KMK -->
+                if (isLeader) {
+                    // KMK <--
                     Text(
-                        text = readProgress,
+                        text = update.mangaTitle,
                         maxLines = 1,
-                        color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA),
+                        style = MaterialTheme.typography.bodyMedium,
+                        color = LocalContentColor.current.copy(alpha = textAlpha),
                         overflow = TextOverflow.Ellipsis,
                     )
                 }
+
+                Row(verticalAlignment = Alignment.CenterVertically) {
+                    var textHeight by remember { mutableIntStateOf(0) }
+                    if (!update.read) {
+                        Icon(
+                            imageVector = Icons.Filled.Circle,
+                            contentDescription = stringResource(MR.strings.unread),
+                            modifier = Modifier
+                                .height(8.dp)
+                                .padding(end = 4.dp),
+                            tint = MaterialTheme.colorScheme.primary,
+                        )
+                    }
+                    if (update.bookmark) {
+                        Icon(
+                            imageVector = Icons.Filled.Bookmark,
+                            contentDescription = stringResource(MR.strings.action_filter_bookmarked),
+                            modifier = Modifier
+                                .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
+                            tint = MaterialTheme.colorScheme.primary,
+                        )
+                        Spacer(modifier = Modifier.width(2.dp))
+                    }
+                    Text(
+                        text = update.chapterName,
+                        maxLines = 1,
+                        style = MaterialTheme.typography.bodySmall,
+                        color = LocalContentColor.current.copy(alpha = textAlpha),
+                        overflow = TextOverflow.Ellipsis,
+                        onTextLayout = { textHeight = it.size.height },
+                        modifier = Modifier
+                            .weight(weight = 1f, fill = false),
+                    )
+                    if (readProgress != null) {
+                        DotSeparatorText()
+                        Text(
+                            text = readProgress,
+                            maxLines = 1,
+                            color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA),
+                            overflow = TextOverflow.Ellipsis,
+                        )
+                    }
+                }
             }
-        }
 
-        // KMK -->
-        if (isLeader && isExpandable) {
-            CollapseButton(
-                expanded = expanded,
-                collapseToggle = { collapseToggle(update.groupByDateAndManga()) },
+            // KMK -->
+            if (isLeader && isExpandable) {
+                CollapseButton(
+                    expanded = expanded,
+                    collapseToggle = { collapseToggle(update.groupByDateAndManga()) },
+                )
+            }
+            // KMK <--
+
+            ChapterDownloadIndicator(
+                enabled = onDownloadChapter != null,
+                modifier = Modifier.padding(start = 4.dp),
+                downloadStateProvider = downloadStateProvider,
+                downloadProgressProvider = downloadProgressProvider,
+                onClick = { onDownloadChapter?.invoke(it) },
             )
         }
-        // KMK <--
-
-        ChapterDownloadIndicator(
-            enabled = onDownloadChapter != null,
-            modifier = Modifier.padding(start = 4.dp),
-            downloadStateProvider = downloadStateProvider,
-            downloadProgressProvider = downloadProgressProvider,
-            onClick = { onDownloadChapter?.invoke(it) },
-        )
     }
 }
 
@@ -412,6 +473,6 @@ fun CollapseButton(
 
 private val IndicatorSize = MaterialTheme.padding.large
 
-private val UpdateItemPanoramaWidth = 126.dp // Book cover
-private val UpdateItemWidth = 56.dp
+private val UpdateItemPanoramaWidth = 108.dp // Book cover
+private val UpdateItemWidth = 48.dp
 // KMK <--
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt
index df50187885..f389134f8e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt
@@ -432,6 +432,52 @@ class UpdatesScreenModel(
             )
         }
     }
+
+    val chapterSwipeStartAction by libraryPreferences.swipeToEndAction().asState(screenModelScope)
+    val chapterSwipeEndAction by libraryPreferences.swipeToStartAction().asState(screenModelScope)
+
+    /**
+     * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
+     */
+    fun updateSwipe(updateItem: UpdatesItem, swipeAction: LibraryPreferences.ChapterSwipeAction) {
+        screenModelScope.launch {
+            executeUpdateSwipeAction(updateItem, swipeAction)
+        }
+    }
+
+    /**
+     * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
+     */
+    private fun executeUpdateSwipeAction(
+        updateItem: UpdatesItem,
+        swipeAction: LibraryPreferences.ChapterSwipeAction,
+    ) {
+        val update = updateItem.update
+        when (swipeAction) {
+            LibraryPreferences.ChapterSwipeAction.ToggleRead -> {
+                markUpdatesRead(listOf(updateItem), !update.read)
+            }
+            LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> {
+                bookmarkUpdates(listOf(updateItem), !update.bookmark)
+            }
+            LibraryPreferences.ChapterSwipeAction.Download -> {
+                val downloadAction = when (updateItem.downloadStateProvider()) {
+                    Download.State.ERROR,
+                    Download.State.NOT_DOWNLOADED,
+                    -> ChapterDownloadAction.START_NOW
+                    Download.State.QUEUE,
+                    Download.State.DOWNLOADING,
+                    -> ChapterDownloadAction.CANCEL
+                    Download.State.DOWNLOADED -> ChapterDownloadAction.DELETE
+                }
+                downloadChapters(
+                    items = listOf(updateItem),
+                    action = downloadAction,
+                )
+            }
+            LibraryPreferences.ChapterSwipeAction.Disabled -> throw IllegalStateException()
+        }
+    }
     // KMK <--
 
     @Immutable
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt
index 99592ed308..a8294f36cd 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt
@@ -88,6 +88,11 @@ data object UpdatesTab : Tab {
             onMultiBookmarkClicked = screenModel::bookmarkUpdates,
             onMultiMarkAsReadClicked = screenModel::markUpdatesRead,
             onMultiDeleteClicked = screenModel::showConfirmDeleteChapters,
+            // KMK -->
+            updateSwipeStartAction = screenModel.chapterSwipeStartAction,
+            updateSwipeEndAction = screenModel.chapterSwipeEndAction,
+            onUpdateSwipe = screenModel::updateSwipe,
+            // KMK <--
             onUpdateSelected = screenModel::toggleSelection,
             onOpenChapter = {
                 val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId)


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
Hello! Gemini here, providing a summary of this pull request. This PR introduces swipe actions to the chapter updates screen, allowing users to perform quick actions like marking as read/unread, bookmarking/unbookmarking, or managing downloads directly from the updates list by swiping left or right on an item. The specific actions for each swipe direction are configurable via user preferences.

### Highlights
* **Swipe Actions on Updates Screen**: Adds the ability to swipe left or right on individual chapter update items in the Updates screen to perform predefined actions.
* **Configurable Swipe Actions**: The actions triggered by swiping left or right are determined by user preferences, leveraging existing library swipe action settings.
* **Integration with Existing Components**: Reuses and adapts existing components like `MangaChapterListItem`'s swipe action logic and `ChapterDownloadIndicator` within the Updates screen context.

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

* **app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt**
  * Changed `IndicatorSize` visibility from `private` to `internal` to be accessible for swipe action icon sizing.
* **app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt**
  * Imported `size` modifier.
  * Changed `getSwipeAction` visibility from `private` to `internal`.
  * Modified the `swipeAction` composable to apply padding and size (`IndicatorSize`) to the icon.
  * Changed `swipeActionThreshold` visibility from `private` to `internal`.
* **app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt**
  * Imported `LibraryPreferences`.
  * Added parameters for `updateSwipeStartAction`, `updateSwipeEndAction`, and `onUpdateSwipe` to the `UpdateScreen` composable.
  * Passed the new swipe action parameters down to the `updatesUiItems` function.
* **app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt**
  * Imported necessary components for swipe actions (`clipToBounds`, `getSwipeAction`, `swipeActionThreshold`, `SwipeableActionsBox`, `LibraryPreferences`).
  * Added parameters for `updateSwipeStartAction`, `updateSwipeEndAction`, and `onUpdateSwipe` to `updatesUiItems` and `UpdatesUiItem` composables.
  * Wrapped the main `Row` content of `UpdatesUiItem` with `SwipeableActionsBox` to enable swipe gestures.
  * Implemented the logic to define start and end swipe actions using the imported `getSwipeAction` and `swipeActionThreshold`.
* **app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt**
  * Added state properties `chapterSwipeStartAction` and `chapterSwipeEndAction` to read swipe preferences.
  * Added `updateSwipe` function to handle the swipe action triggered by the UI.
  * Added `executeUpdateSwipeAction` private function containing the core logic for performing the selected swipe action (ToggleRead, ToggleBookmark, Download) on an update item.
* **app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt**
  * Passed the `chapterSwipeStartAction`, `chapterSwipeEndAction`, and `updateSwipe` properties from the `screenModel` to the `UpdateScreen` composable.

</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 list of new reads,
> A simple swipe succeeds,
> Marked, saved, or gone.


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T03:27:32Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request effectively introduces swipe actions for chapter updates, enhancing user interaction on the updates screen. The changes are well-structured, with clear separation of concerns between UI components and the screen model.

The modifications to existing components to allow for shared functionality (like making `IndicatorSize` and `getSwipeAction` `internal`) are logical. The implementation of `SwipeableActionsBox` in `UpdatesUiItem` appears correct, and the handling of different swipe actions in `UpdatesScreenModel` is robust.

Overall, the code quality is good, and the new feature is a valuable addition. I have a couple of minor observations that are below the threshold for direct comments based on the review settings, but are noted in the summary.

### Summary of Findings
* **Developer Comments (KMK)**: There are several `// KMK -->` and `// KMK <--` comments throughout the changed files. These appear to be developer markers, possibly related to a specific fork or feature set. While they don't affect functionality, it's generally good practice to remove such temporary or fork-specific markers from general-purpose PRs unless they serve a permanent, documented purpose. This was not commented on directly due to review settings (low severity).
* **Modifier.clipToBounds() Usage**: In `app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt` (line 271), `Modifier.clipToBounds()` is applied to the `SwipeableActionsBox`. It would be good to confirm if this is strictly necessary for the `SwipeableActionsBox`'s behavior or if the library handles its own clipping, especially in relation to potential item decorations like rounded corners. This is a minor query and was not commented on directly due to review settings (low severity).

### Merge Readiness
The pull request is in good shape and implements the swipe actions feature effectively. The identified findings are of low severity and can be addressed at the author's discretion or based on project conventions regarding developer comments. From a technical standpoint, the core changes look solid. As I am an AI, I am not authorized to approve pull requests; please ensure other reviewers approve this code before merging.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T03:28:58Z
user: login: sourcery-ai[bot]
body: Hey @cuong-tran - I've reviewed your changes - here's some feedback:

- Consider extracting the core row content into its own composable so the SwipeableActionsBox wrapper doesn’t force you to duplicate most of the UI layout.
- Memoize your `getSwipeAction(...)` invocations with Compose’s `remember` (keyed on the action and state) to avoid recreating those objects on every recomposition.
- Filter out the Disabled swipe action before passing it to `startActions`/`endActions` to avoid throwing an exception at runtime—only include valid actions in the list.

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

- 🟡 **General issues**: 2 issues found
- 🟢 **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-16T03:35:08Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This is a well-executed pull request that successfully introduces swipe actions to the updates screen, enhancing user interaction for managing chapter updates. The changes are thoughtfully implemented across the UI and ViewModel layers.

Here are some highlights of what was done well:
*   **Clear Separation of Concerns**: The swipe action logic is well-integrated into the `UpdatesScreenModel`, keeping the UI components focused on presentation.
*   **Reusability**: Making `getSwipeAction`, `swipeActionThreshold`, and `IndicatorSize` `internal` promotes consistency and code reuse between different list item implementations (`MangaChapterListItem` and `UpdatesUiItem`).
*   **Performance**: The use of `remember` with appropriate keys for swipe actions in composables is a good practice to prevent unnecessary recalculations.
*   **Consistency**: The swipe action behavior and appearance are consistent with those in the manga chapter list.
*   **Debuggability**: The addition of the `HIDE_COVER_IMAGE_ONLY_SHOW_COLOR` debug toggle is a nice touch for development and testing.

I have a few minor observations that are below the threshold for formal review comments based on the repository's settings, but are worth noting for general awareness (see `findings_summary`).

Overall, the code is clear, well-structured, and achieves the stated goals effectively. Great job!

### Summary of Findings
* **Developer Comments**: The code contains several `// KMK -->` and `// KMK <--` comments. While these can be useful during development, it's generally good practice to remove them or replace them with standard, descriptive comments before merging to the main branch, unless they serve a specific, long-term explanatory purpose. (Severity: low, not commented due to review settings)
* **Clarity of Constant Naming**: In `MangaCover.kt`, the constants `COVER_TEMPLATE_SIZE_BIG` (16.dp), `COVER_TEMPLATE_SIZE_MEDIUM` (24.dp), and `COVER_TEMPLATE_SIZE_NORMAL` (32.dp) are used for sizing placeholder icons. The naming might be slightly counter-intuitive as `Size.Big` results in the smallest icon (16.dp) and `Size.Normal` (the default) results in the largest (32.dp). While this is consistent within the file, it could be a minor point of confusion. (Severity: low, not commented due to review settings)
* **Padding Adjustment in UpdatesUiItem**: In `UpdatesUiItem.kt`, the padding for leader items was adjusted. The new structure `.padding(top = if (isLeader) MaterialTheme.padding.small else 0.dp).padding(vertical = if (isLeader) MaterialTheme.padding.extraSmall else 0.dp, ...)` results in leader items having `MaterialTheme.padding.small + MaterialTheme.padding.extraSmall` top padding. This appears to be an intentional UI adjustment to provide more visual separation for new manga groups and is a reasonable design choice. (Severity: low, informational, not commented due to review settings)

### Merge Readiness
The pull request is in good shape and implements the new features effectively. The code quality is high, and the changes are well-organized. Based on my review, there are no critical or high-severity issues that would block merging. The minor points noted in the `findings_summary` can be addressed at the author's discretion. As I am an AI, I am not authorized to approve pull requests; please ensure other reviewers approve this before merging.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T06:55:04Z
user: login: sourcery-ai[bot]
body: Hey @cuong-tran - I've reviewed your changes - here's some feedback:

- UpdatesUiItem has become very large—consider extracting the swipe‐action setup and the Row+Column content into smaller composables to improve readability and maintainability.
- The conditional cover rendering logic (MangaCover vs. MangaCoverHide vs. Panorama) is duplicated across Updates and History screens—factor this into a shared composable or helper to DRY up the code.
- Hard-coded size constants (e.g. UpdateItemPanoramaWidth, UpdateItemWidth, swipeActionThreshold) are scattered—centralize these in a common dimensions/constants file or theme for consistency.

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

- 🟢 **General issues**: 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-16T07:01:58Z

Review comments:
diff_hunk: @@ -432,6 +432,52 @@ class UpdatesScreenModel(
             )
         }
     }
+
+    val chapterSwipeStartAction by libraryPreferences.swipeToEndAction().asState(screenModelScope)
path: app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt
user: login: sourcery-ai[bot]
body: **issue (bug_risk):** Start/end swipe actions appear swapped

Swap the bindings: chapterSwipeStartAction should use swipeToStartAction, and chapterSwipeEndAction should use swipeToEndAction to match their names and gestures.
created_at: 2025-05-16T03:35:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -231,152 +249,179 @@ private fun UpdatesUiItem(
     val haptic = LocalHapticFeedback.current
     val textAlpha = if (update.read) DISABLED_ALPHA else 1f
 
-    Row(
-        modifier = modifier
-            .selectedBackground(selected)
-            .combinedClickable(
-                onClick = onClick,
-                onLongClick = {
-                    onLongClick()
-                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)
-                },
-            )
-            .padding(
-                // KMK -->
-                vertical = if (isLeader) MaterialTheme.padding.extraSmall else 0.dp,
-                // KMK <--
-                horizontal = MaterialTheme.padding.medium,
-            ),
-        verticalAlignment = Alignment.CenterVertically,
+    // KMK -->
+    val start = getSwipeAction(
+        action = updateSwipeStartAction,
+        read = update.read,
+        bookmark = update.bookmark,
+        downloadState = downloadStateProvider(),
+        background = MaterialTheme.colorScheme.primaryContainer,
+        onSwipe = { onUpdateSwipe(updateSwipeStartAction) },
+    )
+    val end = getSwipeAction(
+        action = updateSwipeEndAction,
+        read = update.read,
+        bookmark = update.bookmark,
+        downloadState = downloadStateProvider(),
+        background = MaterialTheme.colorScheme.primaryContainer,
+        onSwipe = { onUpdateSwipe(updateSwipeEndAction) },
+    )
+
+    SwipeableActionsBox(
path: app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Apply passed-in modifier to the swipeable container

The incoming modifier is applied to the inner Row instead of SwipeableActionsBox, so styling and click handling don’t cover the full swipeable area. Merge it onto the container.

Suggested implementation:

```
    SwipeableActionsBox(
        modifier = modifier.clipToBounds(),
        startActions = listOfNotNull(start),
        endActions = listOfNotNull(end),
        swipeThreshold = swipeActionThreshold,
        backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
    ) {

```

• Inside the `SwipeableActionsBox` content lambda, remove or replace any use of the incoming `modifier` on your inner Row/Gesture container so that only the box itself uses it. For example:

    Row(
-      modifier = modifier
+      modifier = Modifier
         .fillMaxWidth()
         .clickable { /* ... */ },
        verticalAlignment = Alignment.CenterVertically,
    ) { … }

This ensures the full swipeable area (the box) gets your passed-in modifier for layout, background, and click behavior.
created_at: 2025-05-16T03:35:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-16T03:27:15Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This PR adds swipeable gesture support for chapter updates on the Updates screen by propagating user-configured swipe preferences from the ViewModel into the composable items, wrapping each update row in a SwipeableActionsBox with shared utilities, and wiring ViewModel handlers to execute read/bookmark/download actions.

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Wrap update items in a swipeable container and expose swipe parameters | <ul><li>Add updateSwipeStartAction, updateSwipeEndAction and onUpdateSwipe parameters to UpdatesUiItem and UpdateScreen</li><li>Generate start/end swipe actions via remember { getSwipeAction(...) } and use swipeActionThreshold</li><li>Wrap content in SwipeableActionsBox, set clipToBounds and backgroundUntilSwipeThreshold</li><li>Import SwipeableActionsBox, getSwipeAction and swipeActionThreshold utilities</li></ul> | `UpdatesUiItem.kt`<br/>`UpdatesScreen.kt`<br/>`UpdatesTab.kt` |
| Integrate swipe preferences and handlers in the Updates screen ViewModel | <ul><li>Expose chapterSwipeStartAction and chapterSwipeEndAction flows from LibraryPreferences</li><li>Add public updateSwipe(updateItem, swipeAction) to dispatch swipe events</li><li>Implement executeUpdateSwipeAction to toggle read/bookmark or start/cancel/delete downloads based on swipeAction</li></ul> | `UpdatesScreenModel.kt` |
| Extract and unify swipe utilities for chapter list items | <ul><li>Change getSwipeAction from private to internal and expose swipeActionThreshold</li><li>Use remember(...) to cache swipeStart and swipeEnd in MangaChapterListItem</li><li>Adjust icon padding and size using a shared IndicatorSize constant</li></ul> | `MangaChapterListItem.kt` |
| Support conditional cover hiding and update placeholder component | <ul><li>Use DebugToggles.HIDE_COVER_IMAGE_ONLY_SHOW_COLOR to switch between MangaCoverHide and full cover</li><li>Add size parameter to MangaCoverHide and adjust icon sizing based on Size enum</li><li>Refactor HistoryItem to mirror UpdatesUiItem’s cover logic</li></ul> | `MangaCover.kt`<br/>`HistoryItem.kt`<br/>`UpdatesUiItem.kt` |
| Refine layout constants and visibility modifiers | <ul><li>Update constants UpdateItemWidth and UpdateItemPanoramaWidth to new dp values</li><li>Make IndicatorSize internal and clean up related imports</li><li>Apply minor padding and layout cleanups in multiple components</li></ul> | `UpdatesUiItem.kt`<br/>`ChapterDownloadIndicator.kt`<br/>`MangaChapterListItem.kt` |

### Possibly linked issues

- **#0**: The PR implements swipe actions on updates screen for chapter management, fulfilling the issue's feature request.

---

<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-16T06:52:57Z
body: /gemini review
author_association: COLLABORATOR
user: login: cuong-tran
created_at: 2025-05-16T06:53:03Z
body: @sourcery-ai review
author_association: COLLABORATOR