HapeLee před 4 měsíci
rodič
revize
b5acfc3db6

+ 9 - 0
app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt

@@ -65,6 +65,15 @@ class BookInfoActivity : BaseComposeActivity(), VariableDialog.Callback {
             onOpenCharacterNetwork = { bookUrl ->
                 startActivity(MainActivity.createBookCharacterNetworkIntent(this, bookUrl))
             },
+            onOpenCharacterList = { bookUrl ->
+                startActivity(MainActivity.createBookCharacterListIntent(this, bookUrl))
+            },
+            onOpenKnowledgeList = { bookUrl ->
+                startActivity(MainActivity.createBookKnowledgeListIntent(this, bookUrl))
+            },
+            onOpenEventList = { bookUrl ->
+                startActivity(MainActivity.createBookEventListIntent(this, bookUrl))
+            },
         )
     }
 

+ 21 - 14
app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt

@@ -34,7 +34,6 @@ import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.ArrowForward
 import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
-import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Book
 import androidx.compose.material.icons.filled.BookmarkAdd
 import androidx.compose.material.icons.filled.Code
@@ -61,8 +60,8 @@ 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.draw.clip
 import androidx.compose.ui.draw.blur
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.lerp
@@ -84,7 +83,6 @@ import coil.size.Size
 import io.legado.app.R
 import io.legado.app.constant.BookType
 import io.legado.app.data.entities.SearchBook
-import io.legado.app.model.BookCover as BookCoverModel
 import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.ui.main.homepage.modules.BannerModule
@@ -129,6 +127,7 @@ import kotlinx.collections.immutable.toImmutableList
 import kotlinx.coroutines.delay
 import org.koin.compose.koinInject
 import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
+import io.legado.app.model.BookCover as BookCoverModel
 import top.yukonga.miuix.kmp.basic.TopAppBar as MiuixTopAppBar
 
 @OptIn(ExperimentalMaterial3Api::class)
@@ -288,16 +287,10 @@ private fun BookInfoScreenContent(
                                     onViewAllClick = {
                                         onIntent(BookInfoIntent.CharacterListClick)
                                     },
-                                )
-                                BookInfoKnowledge(
-                                    entries = state.knowledgeEntries,
-                                    onViewAllClick = {
+                                    onKnowledgeClick = {
                                         onIntent(BookInfoIntent.KnowledgeListClick)
                                     },
-                                )
-                                BookInfoEvents(
-                                    events = state.recentEvents,
-                                    onViewAllClick = {
+                                    onEventsClick = {
                                         onIntent(BookInfoIntent.EventListClick)
                                     },
                                 )
@@ -1291,6 +1284,8 @@ private fun BookInfoCharacters(
     onCharacterClick: (String) -> Unit,
     onNetworkClick: () -> Unit,
     onViewAllClick: () -> Unit,
+    onKnowledgeClick: () -> Unit,
+    onEventsClick: () -> Unit,
 ) {
     Column(
         modifier = Modifier
@@ -1306,15 +1301,15 @@ private fun BookInfoCharacters(
             verticalAlignment = Alignment.CenterVertically,
         ) {
             AppText(
-                text = stringResource(R.string.book_characters),
+                text = stringResource(R.string.book_info_knowledge),
                 style = LegadoTheme.typography.titleMedium,
                 fontWeight = FontWeight.Bold,
                 modifier = Modifier.weight(1f),
             )
             SmallTonalButton(
                 onClick = onViewAllClick,
-                icon = Icons.AutoMirrored.Filled.ArrowForward,
-                contentDescription = stringResource(R.string.view_all),
+                icon = Icons.AutoMirrored.Outlined.FormatListBulleted,
+                contentDescription = stringResource(R.string.book_characters),
             )
             Spacer(modifier = Modifier.width(4.dp))
             SmallTonalButton(
@@ -1322,6 +1317,18 @@ private fun BookInfoCharacters(
                 icon = Icons.Default.Group,
                 contentDescription = stringResource(R.string.character_network),
             )
+            Spacer(modifier = Modifier.width(4.dp))
+            SmallTonalButton(
+                onClick = onKnowledgeClick,
+                icon = Icons.Default.Book,
+                contentDescription = stringResource(R.string.book_knowledge),
+            )
+            Spacer(modifier = Modifier.width(4.dp))
+            SmallTonalButton(
+                onClick = onEventsClick,
+                icon = Icons.Default.Timeline,
+                contentDescription = stringResource(R.string.plot_events),
+            )
         }
         LazyRow(
             modifier = Modifier.fillMaxWidth(),

+ 98 - 266
app/src/main/java/io/legado/app/ui/book/knowledge/BookCharacterDetailScreen.kt

@@ -1,28 +1,21 @@
 package io.legado.app.ui.book.knowledge
 
-import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.consumeWindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.imePadding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.filled.Person
 import androidx.compose.material.icons.filled.Save
 import androidx.compose.material3.ExperimentalMaterial3Api
@@ -40,6 +33,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import coil.compose.AsyncImage
 import io.legado.app.R
@@ -47,6 +41,7 @@ import io.legado.app.data.entities.BookCharacterProfile
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.TextCard
@@ -65,7 +60,7 @@ import kotlinx.collections.immutable.ImmutableList
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collectLatest
 
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun BookCharacterDetailScreen(
     state: CharacterDetailUiState,
@@ -129,7 +124,7 @@ fun BookCharacterDetailScreen(
     }
 }
 
-@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 private fun CharacterDetailContent(
     state: CharacterDetailUiState,
@@ -160,18 +155,20 @@ private fun CharacterDetailContent(
                 showProfileDialog = true
             },
         )
-        CharacterTagEditor(
+        KnowledgeFlowTagEditor(
+            title = stringResource(R.string.character_tags),
             tags = state.tags,
             onAddTag = { onIntent(CharacterDetailIntent.AddTag(it)) },
             onRemoveTag = { onIntent(CharacterDetailIntent.RemoveTag(it)) },
+            showCloseIcon = true,
         )
-        EditFieldCard(
+        KnowledgeEditFieldCard(
             label = stringResource(R.string.character_personality),
             value = state.personality,
             onValueChange = { onIntent(CharacterDetailIntent.SetPersonality(it)) },
             multiline = true,
         )
-        EditFieldCard(
+        KnowledgeEditFieldCard(
             label = stringResource(R.string.character_summary),
             value = state.summary,
             onValueChange = { onIntent(CharacterDetailIntent.SetSummary(it)) },
@@ -205,44 +202,42 @@ private fun CharacterDetailContent(
         }
     }
 
-    if (showProfileDialog) {
-        AppAlertDialog(
-            show = showProfileDialog,
-            onDismissRequest = { showProfileDialog = false },
-            title = stringResource(R.string.character_detail),
-            confirmText = stringResource(R.string.apply),
-            onConfirm = {
-                onIntent(CharacterDetailIntent.SetName(dialogName))
-                onIntent(CharacterDetailIntent.SetAliasesText(dialogAliases))
-                onIntent(CharacterDetailIntent.SetRole(dialogRole))
-                showProfileDialog = false
-            },
-            dismissText = stringResource(R.string.cancel),
-            onDismiss = { showProfileDialog = false },
-            content = {
-                Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
-                    AppTextField(
-                        value = dialogName,
-                        onValueChange = { dialogName = it },
-                        label = stringResource(R.string.character_name),
-                        modifier = Modifier.fillMaxWidth(),
-                        singleLine = true,
-                    )
-                    AppTextField(
-                        value = dialogAliases,
-                        onValueChange = { dialogAliases = it },
-                        label = stringResource(R.string.character_aliases),
-                        modifier = Modifier.fillMaxWidth(),
-                        singleLine = true,
-                    )
-                    ProfileRoleDropdown(
-                        selectedRole = dialogRole,
-                        onRoleSelected = { dialogRole = it },
-                    )
-                }
-            },
-        )
-    }
+    AppAlertDialog(
+        show = showProfileDialog,
+        onDismissRequest = { showProfileDialog = false },
+        title = stringResource(R.string.character_detail),
+        confirmText = stringResource(R.string.apply),
+        onConfirm = {
+            onIntent(CharacterDetailIntent.SetName(dialogName))
+            onIntent(CharacterDetailIntent.SetAliasesText(dialogAliases))
+            onIntent(CharacterDetailIntent.SetRole(dialogRole))
+            showProfileDialog = false
+        },
+        dismissText = stringResource(R.string.cancel),
+        onDismiss = { showProfileDialog = false },
+        content = {
+            Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+                AppTextField(
+                    value = dialogName,
+                    onValueChange = { dialogName = it },
+                    label = stringResource(R.string.character_name),
+                    modifier = Modifier.fillMaxWidth(),
+                    singleLine = true,
+                )
+                AppTextField(
+                    value = dialogAliases,
+                    onValueChange = { dialogAliases = it },
+                    label = stringResource(R.string.character_aliases),
+                    modifier = Modifier.fillMaxWidth(),
+                    singleLine = true,
+                )
+                ProfileRoleDropdown(
+                    selectedRole = dialogRole,
+                    onRoleSelected = { dialogRole = it },
+                )
+            }
+        },
+    )
 }
 
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -314,18 +309,17 @@ private fun CharacterHeader(
 
     Row(
         modifier = Modifier.fillMaxWidth(),
-        horizontalArrangement = Arrangement.spacedBy(16.dp),
+        horizontalArrangement = Arrangement.spacedBy(12.dp),
         verticalAlignment = Alignment.CenterVertically,
     ) {
         GlassCard(
             onClick = onPickAvatar,
+            cornerRadius = 64.dp,
             containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
         ) {
             Box(
                 modifier = Modifier
-                    .size(72.dp)
-                    .clip(CircleShape)
-                    .background(LegadoTheme.colorScheme.surfaceContainerHighest),
+                    .size(72.dp),
                 contentAlignment = Alignment.Center,
             ) {
                 if (avatarUri.isNotBlank() && !avatarLoadFailed.value) {
@@ -333,8 +327,7 @@ private fun CharacterHeader(
                         model = avatarUri,
                         contentDescription = null,
                         modifier = Modifier
-                            .fillMaxSize()
-                            .clip(CircleShape),
+                            .fillMaxSize(),
                         contentScale = ContentScale.Crop,
                         onError = { avatarLoadFailed.value = true },
                     )
@@ -348,98 +341,44 @@ private fun CharacterHeader(
                 }
             }
         }
-        GlassCard(
-            onClick = onInfoClick,
-            modifier = Modifier.weight(1f),
-            containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+
+        Column(
+            modifier = Modifier
+                .weight(1f)
+                .clip(RoundedCornerShape(12.dp))
+                .clickable(onClick = onInfoClick)
+                .padding(12.dp),
+            verticalArrangement = Arrangement.spacedBy(4.dp),
         ) {
-            Column(
-                modifier = Modifier.padding(12.dp),
-                verticalArrangement = Arrangement.spacedBy(4.dp),
+            Row(
+                verticalAlignment = Alignment.CenterVertically,
+                horizontalArrangement = Arrangement.spacedBy(8.dp),
             ) {
-                Row(
-                    verticalAlignment = Alignment.CenterVertically,
-                    horizontalArrangement = Arrangement.spacedBy(8.dp),
-                ) {
-                    AnimatedTextLine(
-                        text = name.ifBlank { "—" },
-                        style = LegadoTheme.typography.titleMedium,
-                        modifier = Modifier.weight(1f, fill = false),
-                    )
-                    if (resolvedRole.isNotBlank()) {
-                        TextCard(text = resolvedRole)
-                    }
-                }
-                if (aliasesText.isNotBlank()) {
-                    AnimatedTextLine(
-                        text = aliasesText,
-                        style = LegadoTheme.typography.bodySmall,
-                        color = LegadoTheme.colorScheme.onSurfaceVariant,
-                        maxLines = 2,
-                    )
+                AnimatedTextLine(
+                    text = name.ifBlank { "—" },
+                    style = LegadoTheme.typography.titleMedium,
+                    modifier = Modifier.weight(1f, fill = false),
+                )
+                if (resolvedRole.isNotBlank()) {
+                    TextCard(text = resolvedRole)
                 }
             }
-        }
-    }
-}
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-private fun EditFieldCard(
-    label: String,
-    value: String,
-    onValueChange: (String) -> Unit,
-    multiline: Boolean = false,
-    modifier: Modifier = Modifier,
-) {
-    var showDialog by remember { mutableStateOf(false) }
-    var dialogValue by remember(value) { mutableStateOf(value) }
-
-    GlassCard(
-        onClick = {
-            dialogValue = value
-            showDialog = true
-        },
-        modifier = modifier,
-        containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-    ) {
-        Column(modifier = Modifier.padding(16.dp)) {
-            AppText(
-                text = label,
-                style = LegadoTheme.typography.labelMedium,
-                color = LegadoTheme.colorScheme.onSurfaceVariant,
-            )
-            AnimatedTextLine(
-                text = value.ifBlank { "—" },
-                style = LegadoTheme.typography.bodyMedium,
-                color = if (value.isBlank()) LegadoTheme.colorScheme.onSurfaceVariant else LegadoTheme.colorScheme.onSurface,
-            )
-        }
-    }
-
-    if (showDialog) {
-        AppAlertDialog(
-            show = showDialog,
-            onDismissRequest = { showDialog = false },
-            title = label,
-            confirmText = stringResource(R.string.apply),
-            onConfirm = {
-                onValueChange(dialogValue)
-                showDialog = false
-            },
-            dismissText = stringResource(R.string.cancel),
-            onDismiss = { showDialog = false },
-            content = {
-                AppTextField(
-                    value = dialogValue,
-                    onValueChange = { dialogValue = it },
-                    modifier = Modifier.fillMaxWidth(),
-                    singleLine = !multiline,
-                    minLines = if (multiline) 4 else 1,
-                    maxLines = if (multiline) 12 else 1,
+            if (aliasesText.isNotBlank()) {
+                AnimatedTextLine(
+                    text = aliasesText,
+                    style = LegadoTheme.typography.bodySmall,
+                    color = LegadoTheme.colorScheme.onSurfaceVariant,
+                    maxLines = 2,
                 )
-            },
-        )
+            } else {
+                AnimatedTextLine(
+                    text = stringResource(R.string.character_no_aliases),
+                    style = LegadoTheme.typography.bodySmall,
+                    color = LegadoTheme.colorScheme.onSurfaceVariant,
+                    maxLines = 2,
+                )
+            }
+        }
     }
 }
 
@@ -456,11 +395,18 @@ private fun CharacterReadonlySection(
             style = LegadoTheme.typography.titleMedium,
         )
         if (isEmpty) {
-            AnimatedTextLine(
-                text = emptyText,
-                style = LegadoTheme.typography.bodyMedium,
-                color = LegadoTheme.colorScheme.onSurfaceVariant,
-            )
+            GlassCard {
+                Box(
+                    modifier = Modifier
+                        .padding(vertical = 16.dp)
+                        .fillMaxWidth(),
+                    contentAlignment = Alignment.Center
+                ) {
+                    EmptyMessage(
+                        message = emptyText
+                    )
+                }
+            }
         } else {
             content()
         }
@@ -483,129 +429,15 @@ private fun InfoCard(
             AnimatedTextLine(
                 text = title.ifBlank { stringResource(R.string.character_unknown) },
                 style = LegadoTheme.typography.bodyMedium,
+                overflow = TextOverflow.Ellipsis,
             )
             AnimatedTextLine(
                 text = body.ifBlank { stringResource(R.string.character_no_summary) },
                 style = LegadoTheme.typography.bodySmall,
                 color = LegadoTheme.colorScheme.onSurfaceVariant,
+                overflow = TextOverflow.Ellipsis,
                 maxLines = 4,
             )
         }
     }
 }
-
-@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-private fun CharacterTagEditor(
-    tags: ImmutableList<String>,
-    onAddTag: (String) -> Unit,
-    onRemoveTag: (Int) -> Unit,
-) {
-    var showAddDialog by remember { mutableStateOf(false) }
-    var newTagValue by remember { mutableStateOf("") }
-    var showRemoveDialog by remember { mutableStateOf(false) }
-    var removeTagIndex by remember { mutableStateOf(-1) }
-    var removeTagName by remember { mutableStateOf("") }
-
-    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
-        AppText(
-            text = stringResource(R.string.character_tags),
-            style = LegadoTheme.typography.titleMedium,
-        )
-        FlowRow(
-            horizontalArrangement = Arrangement.spacedBy(6.dp),
-            verticalArrangement = Arrangement.spacedBy(6.dp),
-        ) {
-            tags.forEachIndexed { index, tag ->
-                GlassCard(
-                    onClick = {
-                        removeTagIndex = index
-                        removeTagName = tag
-                        showRemoveDialog = true
-                    },
-                    containerColor = LegadoTheme.colorScheme.secondaryContainer,
-                ) {
-                    Row(
-                        modifier = Modifier.padding(
-                            start = 10.dp,
-                            end = 6.dp,
-                            top = 6.dp,
-                            bottom = 6.dp
-                        ),
-                        verticalAlignment = Alignment.CenterVertically,
-                    ) {
-                        AnimatedTextLine(
-                            text = tag,
-                            style = LegadoTheme.typography.labelSmall,
-                            color = LegadoTheme.colorScheme.onSecondaryContainer,
-                        )
-                        Spacer(modifier = Modifier.width(2.dp))
-                        AppIcon(
-                            imageVector = Icons.Default.Close,
-                            contentDescription = null,
-                            modifier = Modifier.size(12.dp),
-                            tint = LegadoTheme.colorScheme.onSecondaryContainer,
-                        )
-                    }
-                }
-            }
-            GlassCard(
-                onClick = {
-                    newTagValue = ""
-                    showAddDialog = true
-                },
-                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-            ) {
-                AppIcon(
-                    imageVector = Icons.Default.Add,
-                    contentDescription = stringResource(R.string.add),
-                    modifier = Modifier
-                        .padding(horizontal = 10.dp, vertical = 6.dp)
-                        .size(14.dp),
-                    tint = LegadoTheme.colorScheme.onSurfaceVariant,
-                )
-            }
-        }
-    }
-
-    if (showAddDialog) {
-        AppAlertDialog(
-            show = showAddDialog,
-            onDismissRequest = { showAddDialog = false },
-            title = stringResource(R.string.character_tags),
-            confirmText = stringResource(R.string.apply),
-            onConfirm = {
-                if (newTagValue.isNotBlank()) {
-                    onAddTag(newTagValue)
-                }
-                showAddDialog = false
-            },
-            dismissText = stringResource(R.string.cancel),
-            onDismiss = { showAddDialog = false },
-            content = {
-                AppTextField(
-                    value = newTagValue,
-                    onValueChange = { newTagValue = it },
-                    modifier = Modifier.fillMaxWidth(),
-                    singleLine = true,
-                )
-            },
-        )
-    }
-
-    if (showRemoveDialog) {
-        AppAlertDialog(
-            show = showRemoveDialog,
-            onDismissRequest = { showRemoveDialog = false },
-            title = stringResource(R.string.character_tags),
-            text = removeTagName,
-            confirmText = stringResource(R.string.delete),
-            onConfirm = {
-                onRemoveTag(removeTagIndex)
-                showRemoveDialog = false
-            },
-            dismissText = stringResource(R.string.cancel),
-            onDismiss = { showRemoveDialog = false },
-        )
-    }
-}

+ 30 - 46
app/src/main/java/io/legado/app/ui/book/knowledge/BookCharacterListScreen.kt

@@ -5,17 +5,13 @@ import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.consumeWindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.imePadding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.shape.CircleShape
@@ -23,7 +19,6 @@ import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Person
 import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
@@ -44,17 +39,17 @@ import coil.compose.AsyncImage
 import io.legado.app.R
 import io.legado.app.data.entities.BookCharacterProfile
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppFloatingActionButton
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
+import io.legado.app.ui.widget.components.tabRow.AppTabRow
 import io.legado.app.ui.widget.components.text.AnimatedTextLine
-import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
-import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.flow.Flow
@@ -74,6 +69,19 @@ fun BookCharacterListScreen(
     val context = LocalContext.current
     val lifecycleOwner = LocalLifecycleOwner.current
 
+    val roleFilterLabels = remember {
+        listOf(
+            "" to R.string.knowledge_type_all,
+            BookCharacterProfile.ROLE_MALE_LEAD to R.string.role_male_lead,
+            BookCharacterProfile.ROLE_FEMALE_LEAD to R.string.role_female_lead,
+            BookCharacterProfile.ROLE_MALE_SUPPORTING to R.string.role_male_supporting,
+            BookCharacterProfile.ROLE_FEMALE_SUPPORTING to R.string.role_female_supporting,
+        )
+    }
+    val tabTitles = roleFilterLabels.map { stringResource(it.second) }
+    val selectedTabIndex =
+        roleFilterLabels.indexOfFirst { it.first == state.roleFilter }.coerceAtLeast(0)
+
     LaunchedEffect(effects) {
         effects.collectLatest { effect ->
             when (effect) {
@@ -102,6 +110,15 @@ fun BookCharacterListScreen(
                     TopBarNavigationButton(onClick = onBack)
                 },
                 scrollBehavior = scrollBehavior,
+                bottomContent = {
+                    AppTabRow(
+                        tabTitles = tabTitles,
+                        selectedTabIndex = selectedTabIndex,
+                        onTabSelected = { index ->
+                            onIntent(CharacterListIntent.SetRoleFilter(roleFilterLabels[index].first))
+                        }
+                    )
+                }
             )
         },
         floatingActionButton = {
@@ -127,9 +144,8 @@ fun BookCharacterListScreen(
                 onIntent = onIntent,
                 modifier = Modifier
                     .fillMaxSize()
-                    .padding(paddingValues)
-                    .consumeWindowInsets(paddingValues)
                     .imePadding(),
+                paddingValues = paddingValues
             )
         }
     }
@@ -141,48 +157,16 @@ private fun CharacterListContent(
     state: CharacterListUiState,
     onIntent: (CharacterListIntent) -> Unit,
     modifier: Modifier = Modifier,
+    paddingValues: PaddingValues
 ) {
-    val roleFilterLabels = mapOf(
-        "" to stringResource(R.string.knowledge_type_all),
-        BookCharacterProfile.ROLE_MALE_LEAD to stringResource(R.string.role_male_lead),
-        BookCharacterProfile.ROLE_FEMALE_LEAD to stringResource(R.string.role_female_lead),
-        BookCharacterProfile.ROLE_MALE_SUPPORTING to stringResource(R.string.role_male_supporting),
-        BookCharacterProfile.ROLE_FEMALE_SUPPORTING to stringResource(R.string.role_female_supporting),
-    )
-
     LazyColumn(
         modifier = modifier,
-        contentPadding = PaddingValues(16.dp),
+        contentPadding = adaptiveContentPadding(
+            top = paddingValues.calculateTopPadding(),
+            bottom = 120.dp
+        ),
         verticalArrangement = Arrangement.spacedBy(8.dp),
     ) {
-        item {
-            FlowRow(
-                horizontalArrangement = Arrangement.spacedBy(8.dp),
-                verticalArrangement = Arrangement.spacedBy(4.dp),
-            ) {
-                roleFilterLabels.forEach { (role, label) ->
-                    GlassCard(
-                        onClick = { onIntent(CharacterListIntent.SetRoleFilter(role)) },
-                        containerColor = if (state.roleFilter == role) {
-                            LegadoTheme.colorScheme.primaryContainer
-                        } else {
-                            LegadoTheme.colorScheme.surfaceContainerLow
-                        },
-                    ) {
-                        AnimatedTextLine(
-                            text = label,
-                            style = LegadoTheme.typography.labelMedium,
-                            modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
-                            color = if (state.roleFilter == role) {
-                                LegadoTheme.colorScheme.onPrimaryContainer
-                            } else {
-                                LegadoTheme.colorScheme.onSurface
-                            },
-                        )
-                    }
-                }
-            }
-        }
         if (state.characters.isEmpty()) {
             item {
                 Box(

+ 7 - 4
app/src/main/java/io/legado/app/ui/book/knowledge/BookCharacterNetworkScreen.kt

@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.consumeWindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.imePadding
@@ -40,6 +39,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
 import coil.compose.AsyncImage
 import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.button.series.MediumTonalButton
@@ -118,9 +118,8 @@ fun BookCharacterNetworkScreen(
                 onIntent = onIntent,
                 modifier = Modifier
                     .fillMaxSize()
-                    .padding(paddingValues)
-                    .consumeWindowInsets(paddingValues)
                     .imePadding(),
+                paddingValues = paddingValues,
             )
         }
     }
@@ -131,10 +130,14 @@ private fun CharacterNetworkContent(
     state: CharacterNetworkUiState,
     onIntent: (CharacterNetworkIntent) -> Unit,
     modifier: Modifier = Modifier,
+    paddingValues: PaddingValues,
 ) {
     LazyColumn(
         modifier = modifier,
-        contentPadding = PaddingValues(16.dp),
+        contentPadding = adaptiveContentPadding(
+            top = paddingValues.calculateTopPadding(),
+            bottom = 120.dp,
+        ),
         verticalArrangement = Arrangement.spacedBy(12.dp),
     ) {
         item {

+ 4 - 75
app/src/main/java/io/legado/app/ui/book/knowledge/BookEventDetailScreen.kt

@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.consumeWindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.imePadding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.rememberScrollState
@@ -13,13 +12,8 @@ import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Save
 import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -27,14 +21,8 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
-import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppScaffold
-import io.legado.app.ui.widget.components.AppTextField
-import io.legado.app.ui.widget.components.alert.AppAlertDialog
-import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
-import io.legado.app.ui.widget.components.text.AnimatedTextLine
-import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
@@ -105,7 +93,6 @@ fun BookEventDetailScreen(
     }
 }
 
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 private fun EventDetailContent(
     state: EventDetailUiState,
@@ -116,22 +103,22 @@ private fun EventDetailContent(
         modifier = modifier.padding(horizontal = 16.dp, vertical = 12.dp),
         verticalArrangement = Arrangement.spacedBy(12.dp),
     ) {
-        EditFieldCard(
+        KnowledgeEditFieldCard(
             label = stringResource(R.string.event_character_name),
             value = state.characterName,
             onValueChange = { onIntent(EventDetailIntent.SetCharacterName(it)) },
         )
-        EditFieldCard(
+        KnowledgeEditFieldCard(
             label = stringResource(R.string.event_chapter_title),
             value = state.chapterTitle,
             onValueChange = { onIntent(EventDetailIntent.SetChapterTitle(it)) },
         )
-        EditFieldCard(
+        KnowledgeEditFieldCard(
             label = stringResource(R.string.event_time_text),
             value = state.eventTimeText,
             onValueChange = { onIntent(EventDetailIntent.SetEventTimeText(it)) },
         )
-        EditFieldCard(
+        KnowledgeEditFieldCard(
             label = stringResource(R.string.event_content),
             value = state.content,
             onValueChange = { onIntent(EventDetailIntent.SetContent(it)) },
@@ -139,61 +126,3 @@ private fun EventDetailContent(
         )
     }
 }
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-private fun EditFieldCard(
-    label: String,
-    value: String,
-    onValueChange: (String) -> Unit,
-    multiline: Boolean = false,
-) {
-    var showDialog by remember { mutableStateOf(false) }
-    var dialogValue by remember(value) { mutableStateOf(value) }
-
-    GlassCard(
-        onClick = {
-            dialogValue = value
-            showDialog = true
-        },
-        containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-    ) {
-        Column(modifier = Modifier.padding(16.dp)) {
-            AppText(
-                text = label,
-                style = LegadoTheme.typography.labelMedium,
-                color = LegadoTheme.colorScheme.onSurfaceVariant,
-            )
-            AnimatedTextLine(
-                text = value.ifBlank { "—" },
-                style = LegadoTheme.typography.bodyMedium,
-                color = if (value.isBlank()) LegadoTheme.colorScheme.onSurfaceVariant else LegadoTheme.colorScheme.onSurface,
-            )
-        }
-    }
-
-    if (showDialog) {
-        AppAlertDialog(
-            show = showDialog,
-            onDismissRequest = { showDialog = false },
-            title = label,
-            confirmText = stringResource(R.string.apply),
-            onConfirm = {
-                onValueChange(dialogValue)
-                showDialog = false
-            },
-            dismissText = stringResource(R.string.cancel),
-            onDismiss = { showDialog = false },
-            content = {
-                AppTextField(
-                    value = dialogValue,
-                    onValueChange = { dialogValue = it },
-                    modifier = Modifier.fillMaxWidth(),
-                    singleLine = !multiline,
-                    minLines = if (multiline) 4 else 1,
-                    maxLines = if (multiline) 12 else 1,
-                )
-            },
-        )
-    }
-}

+ 5 - 8
app/src/main/java/io/legado/app/ui/book/knowledge/BookEventListScreen.kt

@@ -3,8 +3,6 @@ package io.legado.app.ui.book.knowledge
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.consumeWindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.imePadding
@@ -28,16 +26,14 @@ import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.compose.LocalLifecycleOwner
 import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppFloatingActionButton
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.card.GlassCard
-import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
 import io.legado.app.ui.widget.components.text.AnimatedTextLine
-import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
-import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.flow.Flow
@@ -108,10 +104,11 @@ fun BookEventListScreen(
             LazyColumn(
                 modifier = Modifier
                     .fillMaxSize()
-                    .padding(paddingValues)
-                    .consumeWindowInsets(paddingValues)
                     .imePadding(),
-                contentPadding = PaddingValues(16.dp),
+                contentPadding = adaptiveContentPadding(
+                    top = paddingValues.calculateTopPadding(),
+                    bottom = 120.dp,
+                ),
                 verticalArrangement = Arrangement.spacedBy(8.dp),
             ) {
                 if (state.entries.isEmpty()) {

+ 9 - 155
app/src/main/java/io/legado/app/ui/book/knowledge/BookKnowledgeDetailScreen.kt

@@ -3,19 +3,14 @@ package io.legado.app.ui.book.knowledge
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.consumeWindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.imePadding
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Save
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -34,10 +29,7 @@ import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppScaffold
-import io.legado.app.ui.widget.components.AppTextField
-import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.card.GlassCard
-import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
@@ -48,7 +40,6 @@ import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.utils.toastOnUi
-import kotlinx.collections.immutable.ImmutableList
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collectLatest
 
@@ -125,7 +116,6 @@ fun BookKnowledgeDetailScreen(
     }
 }
 
-@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 private fun KnowledgeDetailContent(
     state: KnowledgeDetailUiState,
@@ -140,30 +130,31 @@ private fun KnowledgeDetailContent(
             selectedType = state.type,
             onTypeSelected = { onIntent(KnowledgeDetailIntent.SetType(it)) },
         )
-        EditFieldCard(
+        KnowledgeEditFieldCard(
             label = stringResource(R.string.knowledge_title),
             value = state.title,
             onValueChange = { onIntent(KnowledgeDetailIntent.SetTitle(it)) },
         )
-        KeywordTagEditor(
-            keywords = state.keywords,
-            onAddKeyword = { onIntent(KnowledgeDetailIntent.AddKeyword(it)) },
-            onRemoveKeyword = { onIntent(KnowledgeDetailIntent.RemoveKeyword(it)) },
+        KnowledgeFlowTagEditor(
+            title = stringResource(R.string.knowledge_keywords),
+            tags = state.keywords,
+            onAddTag = { onIntent(KnowledgeDetailIntent.AddKeyword(it)) },
+            onRemoveTag = { onIntent(KnowledgeDetailIntent.RemoveKeyword(it)) },
         )
-        EditFieldCard(
+        KnowledgeEditFieldCard(
             label = stringResource(R.string.knowledge_content),
             value = state.content,
             onValueChange = { onIntent(KnowledgeDetailIntent.SetContent(it)) },
             multiline = true,
         )
         Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
-            EditFieldCard(
+            KnowledgeEditFieldCard(
                 label = stringResource(R.string.knowledge_scope_start),
                 value = state.scopeStartChapter,
                 onValueChange = { onIntent(KnowledgeDetailIntent.SetScopeStart(it)) },
                 modifier = Modifier.weight(1f),
             )
-            EditFieldCard(
+            KnowledgeEditFieldCard(
                 label = stringResource(R.string.knowledge_scope_end),
                 value = state.scopeEndChapter,
                 onValueChange = { onIntent(KnowledgeDetailIntent.SetScopeEnd(it)) },
@@ -173,66 +164,6 @@ private fun KnowledgeDetailContent(
     }
 }
 
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-private fun EditFieldCard(
-    label: String,
-    value: String,
-    onValueChange: (String) -> Unit,
-    multiline: Boolean = false,
-    modifier: Modifier = Modifier,
-) {
-    var showDialog by remember { mutableStateOf(false) }
-    var dialogValue by remember(value) { mutableStateOf(value) }
-
-    GlassCard(
-        onClick = {
-            dialogValue = value
-            showDialog = true
-        },
-        modifier = modifier,
-        containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-    ) {
-        Column(modifier = Modifier.padding(16.dp)) {
-            AppText(
-                text = label,
-                style = LegadoTheme.typography.labelMedium,
-                color = LegadoTheme.colorScheme.onSurfaceVariant,
-            )
-            AnimatedTextLine(
-                text = value.ifBlank { "—" },
-                style = LegadoTheme.typography.bodyMedium,
-                color = if (value.isBlank()) LegadoTheme.colorScheme.onSurfaceVariant else LegadoTheme.colorScheme.onSurface,
-            )
-        }
-    }
-
-    if (showDialog) {
-        AppAlertDialog(
-            show = showDialog,
-            onDismissRequest = { showDialog = false },
-            title = label,
-            confirmText = stringResource(R.string.apply),
-            onConfirm = {
-                onValueChange(dialogValue)
-                showDialog = false
-            },
-            dismissText = stringResource(R.string.cancel),
-            onDismiss = { showDialog = false },
-            content = {
-                AppTextField(
-                    value = dialogValue,
-                    onValueChange = { dialogValue = it },
-                    modifier = Modifier.fillMaxWidth(),
-                    singleLine = !multiline,
-                    minLines = if (multiline) 4 else 1,
-                    maxLines = if (multiline) 12 else 1,
-                )
-            },
-        )
-    }
-}
-
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 private fun KnowledgeTypeSelector(
@@ -275,80 +206,3 @@ private fun KnowledgeTypeSelector(
         }
     }
 }
-
-@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-private fun KeywordTagEditor(
-    keywords: ImmutableList<String>,
-    onAddKeyword: (String) -> Unit,
-    onRemoveKeyword: (Int) -> Unit,
-) {
-    var showAddDialog by remember { mutableStateOf(false) }
-    var newKeywordValue by remember { mutableStateOf("") }
-
-    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
-        AppText(
-            text = stringResource(R.string.knowledge_keywords),
-            style = LegadoTheme.typography.titleMedium,
-        )
-        FlowRow(
-            horizontalArrangement = Arrangement.spacedBy(8.dp),
-            verticalArrangement = Arrangement.spacedBy(6.dp),
-        ) {
-            keywords.forEachIndexed { index, keyword ->
-                GlassCard(
-                    onClick = { onRemoveKeyword(index) },
-                    containerColor = LegadoTheme.colorScheme.secondaryContainer,
-                ) {
-                    AnimatedTextLine(
-                        text = keyword,
-                        style = LegadoTheme.typography.labelMedium,
-                        modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
-                        color = LegadoTheme.colorScheme.onSecondaryContainer,
-                    )
-                }
-            }
-            GlassCard(
-                onClick = {
-                    newKeywordValue = ""
-                    showAddDialog = true
-                },
-                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-            ) {
-                AppIcon(
-                    imageVector = Icons.Default.Add,
-                    contentDescription = stringResource(R.string.add),
-                    modifier = Modifier
-                        .padding(horizontal = 12.dp, vertical = 8.dp)
-                        .size(18.dp),
-                    tint = LegadoTheme.colorScheme.onSurfaceVariant,
-                )
-            }
-        }
-    }
-
-    if (showAddDialog) {
-        AppAlertDialog(
-            show = showAddDialog,
-            onDismissRequest = { showAddDialog = false },
-            title = stringResource(R.string.knowledge_keywords),
-            confirmText = stringResource(R.string.apply),
-            onConfirm = {
-                if (newKeywordValue.isNotBlank()) {
-                    onAddKeyword(newKeywordValue)
-                }
-                showAddDialog = false
-            },
-            dismissText = stringResource(R.string.cancel),
-            onDismiss = { showAddDialog = false },
-            content = {
-                AppTextField(
-                    value = newKeywordValue,
-                    onValueChange = { newKeywordValue = it },
-                    modifier = Modifier.fillMaxWidth(),
-                    singleLine = true,
-                )
-            },
-        )
-    }
-}

+ 5 - 9
app/src/main/java/io/legado/app/ui/book/knowledge/BookKnowledgeListScreen.kt

@@ -3,8 +3,6 @@ package io.legado.app.ui.book.knowledge
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.consumeWindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.imePadding
@@ -13,7 +11,6 @@ import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Info
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
@@ -29,16 +26,14 @@ import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.compose.LocalLifecycleOwner
 import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppFloatingActionButton
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.card.GlassCard
-import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
 import io.legado.app.ui.widget.components.text.AnimatedTextLine
-import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
-import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.flow.Flow
@@ -109,10 +104,11 @@ fun BookKnowledgeListScreen(
             LazyColumn(
                 modifier = Modifier
                     .fillMaxSize()
-                    .padding(paddingValues)
-                    .consumeWindowInsets(paddingValues)
                     .imePadding(),
-                contentPadding = PaddingValues(16.dp),
+                contentPadding = adaptiveContentPadding(
+                    top = paddingValues.calculateTopPadding(),
+                    bottom = 120.dp,
+                ),
                 verticalArrangement = Arrangement.spacedBy(8.dp),
             ) {
                 if (state.entries.isEmpty()) {

+ 240 - 0
app/src/main/java/io/legado/app/ui/book/knowledge/KnowledgeSharedComponents.kt

@@ -0,0 +1,240 @@
+package io.legado.app.ui.book.knowledge
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+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.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.card.GlassCard
+import io.legado.app.ui.widget.components.icon.AppIcon
+import io.legado.app.ui.widget.components.text.AnimatedTextLine
+import io.legado.app.ui.widget.components.text.AppText
+import kotlinx.collections.immutable.ImmutableList
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun KnowledgeEditFieldCard(
+    label: String,
+    value: String,
+    onValueChange: (String) -> Unit,
+    multiline: Boolean = false,
+    modifier: Modifier = Modifier,
+) {
+    var showDialog by remember { mutableStateOf(false) }
+    var dialogValue by remember(value) { mutableStateOf(value) }
+
+    GlassCard(
+        onClick = {
+            dialogValue = value
+            showDialog = true
+        },
+        modifier = modifier,
+        containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+    ) {
+        Column(
+            modifier = Modifier.padding(16.dp),
+            verticalArrangement = Arrangement.spacedBy(4.dp)
+        ) {
+            AppText(
+                text = label,
+                style = LegadoTheme.typography.labelMedium,
+                color = LegadoTheme.colorScheme.onSurfaceVariant,
+            )
+            AnimatedTextLine(
+                text = value.ifBlank { "—" },
+                style = LegadoTheme.typography.bodyMedium,
+                color = if (value.isBlank()) LegadoTheme.colorScheme.onSurfaceVariant else LegadoTheme.colorScheme.onSurface,
+            )
+        }
+    }
+
+    if (showDialog) {
+        AppAlertDialog(
+            show = showDialog,
+            onDismissRequest = { showDialog = false },
+            title = label,
+            confirmText = stringResource(R.string.apply),
+            onConfirm = {
+                onValueChange(dialogValue)
+                showDialog = false
+            },
+            dismissText = stringResource(R.string.cancel),
+            onDismiss = { showDialog = false },
+            content = {
+                AppTextField(
+                    value = dialogValue,
+                    onValueChange = { dialogValue = it },
+                    modifier = Modifier.fillMaxWidth(),
+                    singleLine = !multiline,
+                    minLines = if (multiline) 4 else 1,
+                    maxLines = if (multiline) 12 else 1,
+                )
+            },
+        )
+    }
+}
+
+@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun KnowledgeFlowTagEditor(
+    title: String,
+    tags: ImmutableList<String>,
+    onAddTag: (String) -> Unit,
+    onRemoveTag: (Int) -> Unit,
+    showCloseIcon: Boolean = true,
+) {
+    var showAddDialog by remember { mutableStateOf(false) }
+    var newTagValue by remember { mutableStateOf("") }
+    var showRemoveDialog by remember { mutableStateOf(false) }
+    var removeTagIndex by remember { mutableStateOf(-1) }
+    var removeTagName by remember { mutableStateOf("") }
+
+    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+        AppText(
+            text = title,
+            style = LegadoTheme.typography.titleMedium,
+        )
+        FlowRow(
+            horizontalArrangement = Arrangement.spacedBy(6.dp),
+            verticalArrangement = Arrangement.spacedBy(6.dp),
+            itemVerticalAlignment = Alignment.CenterVertically,
+        ) {
+            tags.forEachIndexed { index, tag ->
+                TagChip(
+                    text = tag,
+                    showCloseIcon = showCloseIcon,
+                    onClick = {
+                        if (showCloseIcon) {
+                            removeTagIndex = index
+                            removeTagName = tag
+                            showRemoveDialog = true
+                        } else {
+                            onRemoveTag(index)
+                        }
+                    },
+                )
+            }
+            GlassCard(
+                onClick = {
+                    newTagValue = ""
+                    showAddDialog = true
+                },
+                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+            ) {
+                Box(
+                    modifier = Modifier.padding(
+                        start = 10.dp,
+                        end = 10.dp,
+                        top = 6.dp,
+                        bottom = 6.dp,
+                    )
+                ) {
+                    AppIcon(
+                        imageVector = Icons.Default.Add,
+                        contentDescription = stringResource(R.string.add),
+                        modifier = Modifier.size(14.dp),
+                        tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                    )
+                }
+            }
+        }
+    }
+
+    AppAlertDialog(
+        show = showAddDialog,
+        onDismissRequest = { showAddDialog = false },
+        title = title,
+        confirmText = stringResource(R.string.apply),
+        onConfirm = {
+            if (newTagValue.isNotBlank()) {
+                onAddTag(newTagValue)
+            }
+            showAddDialog = false
+        },
+        dismissText = stringResource(R.string.cancel),
+        onDismiss = { showAddDialog = false },
+        content = {
+            AppTextField(
+                value = newTagValue,
+                onValueChange = { newTagValue = it },
+                modifier = Modifier.fillMaxWidth(),
+                singleLine = true,
+            )
+        },
+    )
+
+    AppAlertDialog(
+        show = showRemoveDialog,
+        onDismissRequest = { showRemoveDialog = false },
+        title = title,
+        text = removeTagName,
+        confirmText = stringResource(R.string.delete),
+        onConfirm = {
+            onRemoveTag(removeTagIndex)
+            showRemoveDialog = false
+        },
+        dismissText = stringResource(R.string.cancel),
+        onDismiss = { showRemoveDialog = false },
+    )
+
+}
+
+@Composable
+fun TagChip(
+    text: String,
+    showCloseIcon: Boolean = false,
+    onClick: () -> Unit,
+) {
+    GlassCard(
+        onClick = onClick,
+        containerColor = LegadoTheme.colorScheme.secondaryContainer,
+    ) {
+        Row(
+            modifier = Modifier.padding(
+                start = 10.dp,
+                end = 10.dp,
+                top = 6.dp,
+                bottom = 6.dp,
+            ),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            AnimatedTextLine(
+                text = text,
+                style = LegadoTheme.typography.labelSmall,
+                color = LegadoTheme.colorScheme.onSecondaryContainer,
+            )
+            if (showCloseIcon) {
+                Spacer(modifier = Modifier.width(2.dp))
+                AppIcon(
+                    imageVector = Icons.Default.Close,
+                    contentDescription = null,
+                    modifier = Modifier.size(12.dp),
+                    tint = LegadoTheme.colorScheme.onSecondaryContainer,
+                )
+            }
+        }
+    }
+}

+ 4 - 4
app/src/main/java/io/legado/app/ui/widget/components/EmptyMessage.kt

@@ -3,6 +3,7 @@ package io.legado.app.ui.widget.components
 import androidx.annotation.StringRes
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.height
@@ -49,7 +50,8 @@ fun EmptyMessage(
         modifier = modifier
             .wrapContentSize()
             .padding(16.dp),
-        horizontalAlignment = Alignment.CenterHorizontally
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.spacedBy(8.dp)
     ) {
         AnimatedContent(
             targetState = isLoading,
@@ -71,11 +73,9 @@ fun EmptyMessage(
             }
         }
 
-        Spacer(modifier = Modifier.height(8.dp))
-
         AnimatedTextLine(
             text = message,
-            style = LegadoTheme.typography.labelLarge,
+            style = LegadoTheme.typography.labelMediumEmphasized,
             textAlign = TextAlign.Center,
             maxLines = 2,
             softWrap = true,

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

@@ -2484,6 +2484,7 @@
     <string name="about_open_github">打开 GitHub</string>
     <string name="apply">应用</string>
     <string name="book_characters">人物</string>
+    <string name="book_info_knowledge">知识</string>
     <string name="character_network">关系网</string>
     <string name="add_character">新增人物</string>
     <string name="add_character_hint">创建人物档案</string>
@@ -2493,6 +2494,7 @@
     <string name="character_detail">人物详情</string>
     <string name="character_name">人物名称</string>
     <string name="character_aliases">别名</string>
+    <string name="character_no_aliases">无别名</string>
     <string name="character_role">角色</string>
     <string name="character_avatar_uri">头像地址</string>
     <string name="character_avatar_crop">裁剪头像</string>

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

@@ -2489,6 +2489,7 @@
     <string name="about_open_github">Open GitHub</string>
     <string name="apply">Apply</string>
     <string name="book_characters">Characters</string>
+    <string name="book_info_knowledge">Knowledge</string>
     <string name="character_network">Character network</string>
     <string name="add_character">Add character</string>
     <string name="add_character_hint">Create a profile</string>
@@ -2498,6 +2499,7 @@
     <string name="character_detail">Character detail</string>
     <string name="character_name">Name</string>
     <string name="character_aliases">Aliases</string>
+    <string name="character_no_aliases">No aliases</string>
     <string name="character_role">Role</string>
     <string name="character_avatar_uri">Avatar URI</string>
     <string name="character_avatar_crop">Crop avatar</string>