Explorar o código

feat: 详情页删除之

HapeLee hai 1 mes
pai
achega
bde5be6207

+ 20 - 12
app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt

@@ -64,7 +64,7 @@ data class BookInfoUiState(
      *
      *
      * 两种用途:
      * 两种用途:
      * - 本书**未入架**且非空:入架会弹冲突 Sheet,书架按钮提前标成冲突态;
      * - 本书**未入架**且非空:入架会弹冲突 Sheet,书架按钮提前标成冲突态;
-     * - 本书**已入架**且非空:由「书架操作」Sheet 列出,供用户切到另一个副本。
+     * - 本书**已入架**且非空:由删除 Sheet 列出,供用户确认没删错副本 / 切到另一个副本。
      *
      *
      * 判定口径与 [shelfConflict] 完全一致(同一个用例),因此「按钮是冲突态」⟺「点击真的会
      * 判定口径与 [shelfConflict] 完全一致(同一个用例),因此「按钮是冲突态」⟺「点击真的会
      * 弹冲突 Sheet」;具体是哪几本仍由点击后的 Sheet 给出。
      * 弹冲突 Sheet」;具体是哪几本仍由点击后的 Sheet 给出。
@@ -102,7 +102,19 @@ data class BookInfoBookUi(
     val durChapterPos: Int,
     val durChapterPos: Int,
     val remark: String?,
     val remark: String?,
     val intro: String?,
     val intro: String?,
-)
+) {
+    fun toConflictSummary(): ConflictBookSummary = ConflictBookSummary(
+        bookUrl = bookUrl,
+        name = name,
+        author = author,
+        coverUrl = coverPath,
+        customCoverUrl = null,
+        origin = origin,
+        sourceName = originName.ifBlank { origin },
+        totalChapterNum = totalChapterNum,
+        latestChapterTitle = latestChapterTitle,
+    )
+}
 
 
 @Stable
 @Stable
 data class BookInfoSourceUi(
 data class BookInfoSourceUi(
@@ -142,8 +154,9 @@ sealed interface BookInfoSheet {
     data object None : BookInfoSheet
     data object None : BookInfoSheet
     data object CoverPicker : BookInfoSheet
     data object CoverPicker : BookInfoSheet
     data object GroupPicker : BookInfoSheet
     data object GroupPicker : BookInfoSheet
-    /** 已入架书籍的操作面板:其他副本 / 分组 / 删除。 */
-    data object ShelfActions : BookInfoSheet
+
+    /** 已入架书籍的删除 Sheet:其他副本 / 分组 / 删除确认。 */
+    data object ShelfDelete : BookInfoSheet
     data class SourcePicker(val oldBook: Book) : BookInfoSheet
     data class SourcePicker(val oldBook: Book) : BookInfoSheet
     data object ReadRecord : BookInfoSheet
     data object ReadRecord : BookInfoSheet
     data class WebFiles(val openAfterImport: Boolean) : BookInfoSheet
     data class WebFiles(val openAfterImport: Boolean) : BookInfoSheet
@@ -156,7 +169,6 @@ sealed interface BookInfoSheet {
 }
 }
 
 
 sealed interface BookInfoDialog {
 sealed interface BookInfoDialog {
-    data class DeleteBook(val isLocal: Boolean) : BookInfoDialog
     data class EditRemark(val remark: String?) : BookInfoDialog
     data class EditRemark(val remark: String?) : BookInfoDialog
     data class PhotoPreview(val path: String) : BookInfoDialog
     data class PhotoPreview(val path: String) : BookInfoDialog
     data class UnsupportedWebFile(
     data class UnsupportedWebFile(
@@ -200,7 +212,6 @@ sealed interface BookInfoIntent {
     data object ReadRecordClick : BookInfoIntent
     data object ReadRecordClick : BookInfoIntent
     data object RemarkClick : BookInfoIntent
     data object RemarkClick : BookInfoIntent
     data class SaveCover(val path: String) : BookInfoIntent
     data class SaveCover(val path: String) : BookInfoIntent
-    data class ConfirmDelete(val deleteOriginal: Boolean) : BookInfoIntent
     data class UpdateRemark(val remark: String) : BookInfoIntent
     data class UpdateRemark(val remark: String) : BookInfoIntent
     data class SelectGroup(val groupId: Long) : BookInfoIntent
     data class SelectGroup(val groupId: Long) : BookInfoIntent
     data class SelectCover(val coverUrl: String) : BookInfoIntent
     data class SelectCover(val coverUrl: String) : BookInfoIntent
@@ -218,14 +229,11 @@ sealed interface BookInfoIntent {
     data object DismissShelfConflict : BookInfoIntent
     data object DismissShelfConflict : BookInfoIntent
     data class OpenShelfConflictBook(val summary: ConflictBookSummary) : BookInfoIntent
     data class OpenShelfConflictBook(val summary: ConflictBookSummary) : BookInfoIntent
 
 
-    /** 「书架操作」Sheet:切到同名同作者的其他副本。 */
+    /** 删除 Sheet:切到同名同作者的其他副本。 */
     data class OpenShelfDuplicate(val summary: ConflictBookSummary) : BookInfoIntent
     data class OpenShelfDuplicate(val summary: ConflictBookSummary) : BookInfoIntent
 
 
-    /** 「书架操作」Sheet:改分组(进入原有的分组选择 Sheet)。 */
-    data object ShelfActionsGroup : BookInfoIntent
-
-    /** 「书架操作」Sheet:删除本书(仍走原有的确认框 / 删除前提醒设置)。 */
-    data object ShelfActionsDelete : BookInfoIntent
+    /** 删除 Sheet:确认删除本书(Sheet 即确认框,不再弹二次确认)。 */
+    data class ShelfDeleteConfirm(val deleteOriginal: Boolean) : BookInfoIntent
     data class CoexistWithShelfConflict(
     data class CoexistWithShelfConflict(
         val existingBookUrl: String,
         val existingBookUrl: String,
         val options: ChangeSourceMigrationOptions,
         val options: ChangeSourceMigrationOptions,

+ 7 - 35
app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt

@@ -536,12 +536,14 @@ private fun BookInfoScreenContent(
                 onConfirm = { onIntent(BookInfoIntent.SelectGroup(it)) },
                 onConfirm = { onIntent(BookInfoIntent.SelectGroup(it)) },
             )
             )
         }
         }
-        BookInfoSheet.ShelfActions -> ShelfActionsSheet(
-            show = currentSheet == BookInfoSheet.ShelfActions,
+        BookInfoSheet.ShelfDelete -> ShelfDeleteSheet(
+            show = currentSheet == BookInfoSheet.ShelfDelete,
+            book = state.book?.toConflictSummary(),
             copies = state.shelfDuplicates,
             copies = state.shelfDuplicates,
+            isLocal = state.book?.isLocal == true,
+            initialDeleteOriginal = state.deleteOriginal,
             onOpenCopy = { onIntent(BookInfoIntent.OpenShelfDuplicate(it)) },
             onOpenCopy = { onIntent(BookInfoIntent.OpenShelfDuplicate(it)) },
-            onGroup = { onIntent(BookInfoIntent.ShelfActionsGroup) },
-            onDelete = { onIntent(BookInfoIntent.ShelfActionsDelete) },
+            onDelete = { onIntent(BookInfoIntent.ShelfDeleteConfirm(it)) },
             onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
             onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
         )
         )
         is BookInfoSheet.SourcePicker -> {
         is BookInfoSheet.SourcePicker -> {
@@ -1386,7 +1388,7 @@ private fun BookInfoActions(
 
 
     // 未入架但书架里已有同作品副本时,把书架按钮标成冲突态:点击会弹冲突 Sheet,
     // 未入架但书架里已有同作品副本时,把书架按钮标成冲突态:点击会弹冲突 Sheet,
     // 提前告诉用户这一步要问「共存还是迁移」。
     // 提前告诉用户这一步要问「共存还是迁移」。
-    // 已入架时按钮不高亮:可做的事情(副本 / 分组 / 删除)改由点击后的「书架操作」Sheet 给出。
+    // 已入架时按钮不高亮:可做的事情(副本 / 分组 / 删除)改由点击后的删除 Sheet 给出。
     val conflictHighlight = !inBookshelf && hasShelfDuplicates
     val conflictHighlight = !inBookshelf && hasShelfDuplicates
     val shelfLabel = when {
     val shelfLabel = when {
         showShelfRemoveHint -> stringResource(R.string.click_to_remove)
         showShelfRemoveHint -> stringResource(R.string.click_to_remove)
@@ -1991,38 +1993,8 @@ private fun BookInfoDialogs(
     onIntent: (BookInfoIntent) -> Unit,
     onIntent: (BookInfoIntent) -> Unit,
 ) {
 ) {
     val dialog = state.dialog
     val dialog = state.dialog
-    var deleteOriginal by remember(dialog, state.deleteOriginal) { mutableStateOf(state.deleteOriginal) }
     var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) }
     var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) }
 
 
-    AppAlertDialog(
-        data = dialog as? BookInfoDialog.DeleteBook,
-        onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
-        title = stringResource(R.string.draw),
-        text = stringResource(R.string.sure_del),
-        confirmText = stringResource(android.R.string.ok),
-        onConfirm = {
-            onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal))
-        },
-        dismissText = stringResource(android.R.string.cancel),
-        onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
-        content = { d ->
-            if (d.isLocal) {
-                Row(verticalAlignment = Alignment.CenterVertically) {
-                    androidx.compose.material3.Checkbox(
-                        checked = deleteOriginal,
-                        onCheckedChange = { deleteOriginal = it },
-                        colors = androidx.compose.material3.CheckboxDefaults.colors(
-                            checkedColor = LegadoTheme.colorScheme.primary,
-                            checkmarkColor = LegadoTheme.colorScheme.onPrimary,
-                            uncheckedColor = LegadoTheme.colorScheme.onSurfaceVariant,
-                        )
-                    )
-                    Text(text = stringResource(R.string.delete_book_file))
-                }
-            }
-        }
-    )
-
     AppAlertDialog(
     AppAlertDialog(
         data = dialog as? BookInfoDialog.EditRemark,
         data = dialog as? BookInfoDialog.EditRemark,
         onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
         onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },

+ 11 - 20
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -450,11 +450,6 @@ class BookInfoViewModel(
             is BookInfoIntent.SaveCover -> {
             is BookInfoIntent.SaveCover -> {
                 saveCoverToGallery(intent.path)
                 saveCoverToGallery(intent.path)
             }
             }
-            is BookInfoIntent.ConfirmDelete -> {
-                dismissDialog()
-                deleteBook(intent.deleteOriginal)
-            }
-
             is BookInfoIntent.UpdateRemark -> {
             is BookInfoIntent.UpdateRemark -> {
                 dismissDialog()
                 dismissDialog()
                 saveRemark(intent.remark)
                 saveRemark(intent.remark)
@@ -488,9 +483,10 @@ class BookInfoViewModel(
 
 
             is BookInfoIntent.OpenShelfDuplicate -> openShelfDuplicate(intent.summary)
             is BookInfoIntent.OpenShelfDuplicate -> openShelfDuplicate(intent.summary)
 
 
-            BookInfoIntent.ShelfActionsGroup -> setSheet(BookInfoSheet.GroupPicker)
-
-            BookInfoIntent.ShelfActionsDelete -> onShelfActionsDelete()
+            is BookInfoIntent.ShelfDeleteConfirm -> {
+                dismissSheet()
+                deleteBook(intent.deleteOriginal)
+            }
 
 
             is BookInfoIntent.OpenShelfConflictBook -> {
             is BookInfoIntent.OpenShelfConflictBook -> {
                 // 先收起冲突 Sheet 再导航:详情页之间跳转会复用同一份冲突状态,
                 // 先收起冲突 Sheet 再导航:详情页之间跳转会复用同一份冲突状态,
@@ -1395,9 +1391,13 @@ class BookInfoViewModel(
     private fun onShelfClick() {
     private fun onShelfClick() {
         val book = currentBook ?: return
         val book = currentBook ?: return
         if (inBookshelf) {
         if (inBookshelf) {
-            // 已入架:先摊开「其他副本 / 分组 / 删除」。副本要切过去,删除也仍会再确认一次,
-            // 因此这里不再像以前那样点一下就直接问删不删。
-            setSheet(BookInfoSheet.ShelfActions)
+            // 已入架:摊开删除 Sheet(其他副本 / 分组 / 删除确认)。
+            // 关掉「删除前提醒」时按设置直接删,不打断。
+            if (LocalConfig.bookInfoDeleteAlert) {
+                setSheet(BookInfoSheet.ShelfDelete)
+            } else {
+                deleteBook(LocalConfig.deleteBookOriginal)
+            }
         } else if (book.isWebFile) {
         } else if (book.isWebFile) {
             setSheet(BookInfoSheet.WebFiles(openAfterImport = false))
             setSheet(BookInfoSheet.WebFiles(openAfterImport = false))
         } else {
         } else {
@@ -1405,15 +1405,6 @@ class BookInfoViewModel(
         }
         }
     }
     }
 
 
-    private fun onShelfActionsDelete() {
-        dismissSheet()
-        if (LocalConfig.bookInfoDeleteAlert) {
-            showDialog(BookInfoDialog.DeleteBook(currentBook?.isLocal == true))
-        } else {
-            deleteBook(LocalConfig.deleteBookOriginal)
-        }
-    }
-
     private fun openShelfDuplicate(summary: ConflictBookSummary) {
     private fun openShelfDuplicate(summary: ConflictBookSummary) {
         // 先收起 Sheet 再导航:详情页之间跳转会复用同一份 Sheet 状态,
         // 先收起 Sheet 再导航:详情页之间跳转会复用同一份 Sheet 状态,
         // 不收起来的话新页面一进来就显示 Sheet,并把第一次返回键吃掉。
         // 不收起来的话新页面一进来就显示 Sheet,并把第一次返回键吃掉。

+ 0 - 101
app/src/main/java/io/legado/app/ui/book/info/ShelfActionsSheet.kt

@@ -1,101 +0,0 @@
-package io.legado.app.ui.book.info
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material.icons.filled.Group
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import io.legado.app.R
-import io.legado.app.domain.model.ConflictBookSummary
-import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.button.series.MediumTonalButton
-import io.legado.app.ui.widget.components.conflict.ConflictBookCard
-import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
-import io.legado.app.ui.widget.components.text.AppText
-
-/**
- * 已入架书籍的「书架操作」Sheet:其他副本 / 分组 / 删除。
- *
- * 为什么不再直接弹删除确认框:已入架的书可能还有同名同作者的其他副本(共存),
- * 而原来的交互只有「点一下 → 问是否删除」,用户没有别的去处,也没法切到另一个副本。
- * 这里把三种意图平铺出来,删除仍复用原有的确认框(本地书带「删除原文件」选项,
- * 且照旧受「删除前提醒」设置影响)。
- *
- * 长按卡片直接进「分组」的老路保持不变(见 `BookInfoActions`)。
- */
-@Composable
-fun ShelfActionsSheet(
-    show: Boolean,
-    copies: List<ConflictBookSummary>,
-    onOpenCopy: (ConflictBookSummary) -> Unit,
-    onGroup: () -> Unit,
-    onDelete: () -> Unit,
-    onDismissRequest: () -> Unit,
-) {
-    AppModalBottomSheet(
-        show = show,
-        onDismissRequest = onDismissRequest,
-        title = stringResource(R.string.shelf_actions_title),
-    ) {
-        if (copies.isNotEmpty()) {
-            AppText(
-                text = stringResource(R.string.shelf_duplicate_count, copies.size),
-                style = LegadoTheme.typography.titleSmallEmphasized,
-            )
-            AppText(
-                text = stringResource(R.string.bookshelf_conflict_open_hint),
-                style = LegadoTheme.typography.bodySmall,
-                color = LegadoTheme.colorScheme.onSurfaceVariant,
-            )
-            Spacer(modifier = Modifier.height(8.dp))
-            LazyColumn(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .heightIn(max = 320.dp),
-                verticalArrangement = Arrangement.spacedBy(8.dp),
-            ) {
-                items(copies, key = { it.bookUrl }) { copy ->
-                    ConflictBookCard(
-                        summary = copy,
-                        onClick = { onOpenCopy(copy) },
-                        modifier = Modifier.fillMaxWidth(),
-                    )
-                }
-            }
-            Spacer(modifier = Modifier.height(12.dp))
-        }
-        // 两个动作同排、均分宽度。删除不铺底色,用 error 色的图标 + 文字表达危险性,
-        // 免得一个删除按钮在面板里显得比「分组」更重。
-        Row(
-            modifier = Modifier.fillMaxWidth(),
-            // 两个按钮的最小交互高度不同(plain 会被撑到 48dp),居中对齐免得看着错位
-            verticalAlignment = Alignment.CenterVertically,
-            horizontalArrangement = Arrangement.spacedBy(12.dp),
-        ) {
-            MediumTonalButton(
-                onClick = onGroup,
-                modifier = Modifier.weight(1f),
-                icon = Icons.Default.Group,
-                text = stringResource(R.string.group),
-            )
-            MediumTonalButton(
-                onClick = onDelete,
-                modifier = Modifier.weight(1f),
-                icon = Icons.Default.Delete,
-                text = stringResource(R.string.delete),
-            )
-        }
-        Spacer(modifier = Modifier.height(12.dp))
-    }
-}

+ 182 - 0
app/src/main/java/io/legado/app/ui/book/info/ShelfDeleteSheet.kt

@@ -0,0 +1,182 @@
+package io.legado.app.ui.book.info
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.domain.model.ConflictBookSummary
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
+import io.legado.app.ui.widget.components.button.series.MediumTonalButton
+import io.legado.app.ui.widget.components.conflict.ConflictBookCard
+import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.text.AppText
+
+/**
+ * 已入架书籍的删除 Sheet:展示当前书籍确认删除,若有其他副本则给出消歧提示及跳转途径。
+ *
+ * 底部为纯粹的「取消 / 删除」;长按书架卡片直接进入分组选择(保持在详情页常态长按)。
+ */
+@Composable
+fun ShelfDeleteSheet(
+    show: Boolean,
+    book: ConflictBookSummary?,
+    copies: List<ConflictBookSummary>,
+    isLocal: Boolean,
+    initialDeleteOriginal: Boolean,
+    onOpenCopy: (ConflictBookSummary) -> Unit,
+    onDelete: (deleteOriginal: Boolean) -> Unit,
+    onDismissRequest: () -> Unit,
+) {
+    // 纯展示态:每次打开 Sheet 都从设置里的当前值起步,退出组合后随之丢弃。
+    var deleteOriginal by rememberSaveable { mutableStateOf(initialDeleteOriginal) }
+    var showCopiesList by rememberSaveable { mutableStateOf(false) }
+
+    AppModalBottomSheet(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        title = stringResource(R.string.sure_del),
+    ) {
+        book?.let {
+            ConflictBookCard(
+                summary = it,
+                onClick = {},
+                modifier = Modifier.fillMaxWidth(),
+            )
+        }
+        if (copies.isNotEmpty()) {
+            Spacer(modifier = Modifier.height(12.dp))
+            val sourcesText = remember(copies) {
+                copies.take(3).joinToString("】、【") { it.sourceName } +
+                        if (copies.size > 3) "等" else ""
+            }
+            Surface(
+                shape = RoundedCornerShape(12.dp),
+                color = LegadoTheme.colorScheme.secondaryContainer.copy(alpha = 0.45f),
+                modifier = Modifier.fillMaxWidth(),
+            ) {
+                Column(
+                    modifier = Modifier.padding(12.dp),
+                    verticalArrangement = Arrangement.spacedBy(8.dp),
+                ) {
+                    Row(
+                        verticalAlignment = Alignment.Top,
+                        horizontalArrangement = Arrangement.spacedBy(8.dp),
+                    ) {
+                        Icon(
+                            imageVector = Icons.Default.Info,
+                            contentDescription = null,
+                            tint = LegadoTheme.colorScheme.primary,
+                            modifier = Modifier
+                                .size(18.dp)
+                                .padding(top = 2.dp),
+                        )
+                        AppText(
+                            text = stringResource(
+                                R.string.shelf_delete_other_copies_hint,
+                                copies.size,
+                                sourcesText,
+                            ),
+                            style = LegadoTheme.typography.labelSmall,
+                            color = LegadoTheme.colorScheme.onSurface,
+                            modifier = Modifier.weight(1f),
+                        )
+                    }
+                    Row(
+                        modifier = Modifier.fillMaxWidth(),
+                        horizontalArrangement = Arrangement.End,
+                    ) {
+                        if (copies.size == 1) {
+                            MediumTonalButton(
+                                onClick = { onOpenCopy(copies.first()) },
+                                icon = Icons.AutoMirrored.Filled.ArrowForward,
+                                text = stringResource(R.string.shelf_delete_view_copy),
+                            )
+                        } else {
+                            MediumTonalButton(
+                                onClick = { showCopiesList = !showCopiesList },
+                                icon = if (showCopiesList) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
+                                text = stringResource(
+                                    R.string.shelf_delete_view_copies,
+                                    copies.size
+                                ),
+                            )
+                        }
+                    }
+                }
+            }
+
+            if (copies.size > 1 && showCopiesList) {
+                Spacer(modifier = Modifier.height(8.dp))
+                LazyColumn(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .heightIn(max = 240.dp),
+                    verticalArrangement = Arrangement.spacedBy(8.dp),
+                ) {
+                    items(copies, key = { it.bookUrl }) { copy ->
+                        ConflictBookCard(
+                            summary = copy,
+                            onClick = { onOpenCopy(copy) },
+                            modifier = Modifier.fillMaxWidth(),
+                        )
+                    }
+                }
+            }
+        }
+        if (isLocal) {
+            Spacer(modifier = Modifier.height(8.dp))
+            Row(verticalAlignment = Alignment.CenterVertically) {
+                Checkbox(
+                    checked = deleteOriginal,
+                    onCheckedChange = { deleteOriginal = it },
+                    colors = CheckboxDefaults.colors(
+                        checkedColor = LegadoTheme.colorScheme.primary,
+                        checkmarkColor = LegadoTheme.colorScheme.onPrimary,
+                        uncheckedColor = LegadoTheme.colorScheme.onSurfaceVariant,
+                    ),
+                )
+                AppText(
+                    text = stringResource(R.string.delete_book_file),
+                    style = LegadoTheme.typography.bodyMedium,
+                )
+            }
+        }
+        Spacer(modifier = Modifier.height(16.dp))
+        ConfirmDismissButtonsRow(
+            onDismiss = onDismissRequest,
+            onConfirm = { onDelete(deleteOriginal) },
+            dismissText = stringResource(R.string.cancel),
+            confirmText = stringResource(R.string.delete),
+        )
+        Spacer(modifier = Modifier.height(12.dp))
+    }
+}

+ 4 - 2
app/src/main/res/values-zh-rCN/strings.xml

@@ -3405,9 +3405,11 @@
     <!-- 详情页「加入书架」按钮的冲突态文案(按钮上的文字 / 读屏描述) -->
     <!-- 详情页「加入书架」按钮的冲突态文案(按钮上的文字 / 读屏描述) -->
     <string name="bookshelf_conflict_hint">疑似重复</string>
     <string name="bookshelf_conflict_hint">疑似重复</string>
     <string name="bookshelf_conflict_hint_desc">疑似重复,点击加入书架</string>
     <string name="bookshelf_conflict_hint_desc">疑似重复,点击加入书架</string>
-    <!-- 已入架书籍的「书架操作」Sheet:标题与副本列表的分组标题 -->
-    <string name="shelf_actions_title">书架操作</string>
+    <!-- 已入架书籍的删除 Sheet:其他副本提示与跳转 -->
     <string name="shelf_duplicate_count">其他 %1$d 个副本</string>
     <string name="shelf_duplicate_count">其他 %1$d 个副本</string>
+    <string name="shelf_delete_other_copies_hint">书架中仍保留有该书的其他 %1$d 个副本(来自【%2$s】),删除当前副本不影响其他副本。</string>
+    <string name="shelf_delete_view_copy">查看该副本</string>
+    <string name="shelf_delete_view_copies">查看副本 (%1$d)</string>
     <string name="bookshelf_conflict_existing_title">书架中已有的作品</string>
     <string name="bookshelf_conflict_existing_title">书架中已有的作品</string>
     <string name="bookshelf_conflict_open_hint">点击卡片查看该作品详情</string>
     <string name="bookshelf_conflict_open_hint">点击卡片查看该作品详情</string>
     <string name="bookshelf_conflict_coexist">共存</string>
     <string name="bookshelf_conflict_coexist">共存</string>

+ 4 - 2
app/src/main/res/values/strings.xml

@@ -3409,9 +3409,11 @@
     <!-- 详情页「加入书架」按钮的冲突态文案(按钮上的文字 / 读屏描述) -->
     <!-- 详情页「加入书架」按钮的冲突态文案(按钮上的文字 / 读屏描述) -->
     <string name="bookshelf_conflict_hint">Possible Duplicate</string>
     <string name="bookshelf_conflict_hint">Possible Duplicate</string>
     <string name="bookshelf_conflict_hint_desc">Possible duplicate, tap to add to bookshelf</string>
     <string name="bookshelf_conflict_hint_desc">Possible duplicate, tap to add to bookshelf</string>
-    <!-- 已入架书籍的「书架操作」Sheet:标题与副本列表的分组标题 -->
-    <string name="shelf_actions_title">On-shelf Actions</string>
+    <!-- Shelf delete sheet: other copies hint and view -->
     <string name="shelf_duplicate_count">Other copies: %1$d</string>
     <string name="shelf_duplicate_count">Other copies: %1$d</string>
+    <string name="shelf_delete_other_copies_hint">The bookshelf still retains %1$d other copy of this book (from [%2$s]). Deleting this copy will not affect other copies.</string>
+    <string name="shelf_delete_view_copy">View Copy</string>
+    <string name="shelf_delete_view_copies">View Copies (%1$d)</string>
     <string name="bookshelf_conflict_existing_title">Already on bookshelf</string>
     <string name="bookshelf_conflict_existing_title">Already on bookshelf</string>
     <string name="bookshelf_conflict_open_hint">Tap a book to open its details</string>
     <string name="bookshelf_conflict_open_hint">Tap a book to open its details</string>
     <string name="bookshelf_conflict_coexist">Keep Both</string>
     <string name="bookshelf_conflict_coexist">Keep Both</string>