diff --git a/app/shared/app-data/src/commonMain/kotlin/data/models/subject/SubjectInfo.kt b/app/shared/app-data/src/commonMain/kotlin/data/models/subject/SubjectInfo.kt
index 01048ff85a..396d630b91 100644
--- a/app/shared/app-data/src/commonMain/kotlin/data/models/subject/SubjectInfo.kt
+++ b/app/shared/app-data/src/commonMain/kotlin/data/models/subject/SubjectInfo.kt
@@ -151,6 +151,10 @@ data class SubjectInfo(
@Stable
val SubjectInfo.nameCnOrName get() = nameCn.takeIf { it.isNotBlank() } ?: name
+@Stable
+val SubjectInfo.fullDisplayName: String
+ get() = if (nameCn.isNotBlank() && name.isNotBlank() && nameCn != name) "$nameCn / $name" else displayName
+
fun SubjectInfo.toNavPlaceholder(): SubjectDetailPlaceholder {
return SubjectDetailPlaceholder(subjectId, name, nameCn, imageLarge)
}
diff --git a/app/shared/app-lang/src/androidMain/res/values-zh-rCN/strings.xml b/app/shared/app-lang/src/androidMain/res/values-zh-rCN/strings.xml
index dafff737cb..970608287e 100644
--- a/app/shared/app-lang/src/androidMain/res/values-zh-rCN/strings.xml
+++ b/app/shared/app-lang/src/androidMain/res/values-zh-rCN/strings.xml
@@ -278,6 +278,8 @@
导出配置
已复制到剪贴板
目前无法导出,请稍后再试
+ 番名已复制到剪贴板
+
导出单个配置 (仅限开发者)
diff --git a/app/shared/app-lang/src/androidMain/res/values-zh-rHK/strings.xml b/app/shared/app-lang/src/androidMain/res/values-zh-rHK/strings.xml
index a9a81cab6f..f01eb0914b 100644
--- a/app/shared/app-lang/src/androidMain/res/values-zh-rHK/strings.xml
+++ b/app/shared/app-lang/src/androidMain/res/values-zh-rHK/strings.xml
@@ -278,6 +278,8 @@
導出配置
已複製到剪貼板
目前無法導出,請稍後再試
+ 番名已複製到剪貼板
+
導出單個配置 (僅限開發者)
diff --git a/app/shared/app-lang/src/androidMain/res/values-zh-rTW/strings.xml b/app/shared/app-lang/src/androidMain/res/values-zh-rTW/strings.xml
index 076ddfe765..3ae1df3687 100644
--- a/app/shared/app-lang/src/androidMain/res/values-zh-rTW/strings.xml
+++ b/app/shared/app-lang/src/androidMain/res/values-zh-rTW/strings.xml
@@ -278,6 +278,8 @@
匯出配置
已複製到剪貼簿
目前無法匯出,請稍後再試
+ 番名已複製到剪貼簿
+
匯出單個配置 (僅限開發者)
diff --git a/app/shared/app-lang/src/androidMain/res/values/strings.xml b/app/shared/app-lang/src/androidMain/res/values/strings.xml
index 4bb5714dab..1f70b7c00b 100644
--- a/app/shared/app-lang/src/androidMain/res/values/strings.xml
+++ b/app/shared/app-lang/src/androidMain/res/values/strings.xml
@@ -254,6 +254,8 @@
Export configuration
Copied to clipboard
Unable to export now, please try again later
+ Title copied to clipboard
+
Export a single configuration (developer only)
Refresh
Unauthorized
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/SubjectDetailsPage.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/SubjectDetailsPage.kt
index e630224979..d51bda5ef5 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/SubjectDetailsPage.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/SubjectDetailsPage.kt
@@ -12,6 +12,7 @@ package me.him188.ani.app.ui.subject.details
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -69,8 +70,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -84,6 +87,7 @@ import me.him188.ani.app.data.models.subject.SubjectCollectionStats
import me.him188.ani.app.data.models.subject.SubjectInfo
import me.him188.ani.app.data.models.subject.SubjectProgressInfo
import me.him188.ani.app.data.models.subject.Tag
+import me.him188.ani.app.data.models.subject.fullDisplayName
import me.him188.ani.app.domain.episode.SetEpisodeCollectionTypeRequest
import me.him188.ani.app.domain.foundation.LoadError
import me.him188.ani.app.navigation.LocalNavigator
@@ -114,6 +118,8 @@ import me.him188.ani.app.ui.foundation.theme.MaterialThemeFromImage
import me.him188.ani.app.ui.foundation.toComposeImageBitmap
import me.him188.ani.app.ui.foundation.widgets.LocalToaster
import me.him188.ani.app.ui.foundation.widgets.showLoadError
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_title_copied
import me.him188.ani.app.ui.rating.EditableRating
import me.him188.ani.app.ui.rating.EditableRatingState
import me.him188.ani.app.ui.richtext.RichTextDefaults
@@ -136,6 +142,7 @@ import me.him188.ani.app.ui.user.SelfInfoUiState
import me.him188.ani.datasources.api.PackedDate
import me.him188.ani.datasources.api.topic.toggleCollected
import me.him188.ani.utils.platform.isMobile
+import org.jetbrains.compose.resources.stringResource
// region screen
@@ -552,10 +559,26 @@ fun SubjectDetailsLayout(
AniAnimatedVisibility(connectedScrollState.isScrolledTop) {
TopAppBar(
title = {
+ val clipboard = LocalClipboardManager.current
+ val toaster = LocalToaster.current
+ val copied = stringResource(Lang.subject_title_copied)
+ val fullName = info?.fullDisplayName.orEmpty()
+ val copy = {
+ if (fullName.isNotBlank()) {
+ clipboard.setText(AnnotatedString(fullName))
+ toaster.toast(copied)
+ }
+ }
Text(
info?.displayName ?: "",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.combinedClickable(
+ onClick = copy,
+ onLongClick = copy,
+ onLongClickLabel = "复制",
+ onClickLabel = "复制",
+ ),
)
},
navigationIcon = navigationIcon,
diff --git a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/SubjectDetailsHeader.kt b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/SubjectDetailsHeader.kt
index 5174959725..ce9a01b79d 100644
--- a/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/SubjectDetailsHeader.kt
+++ b/app/shared/ui-subject/src/commonMain/kotlin/ui/subject/details/components/SubjectDetailsHeader.kt
@@ -9,7 +9,7 @@
package me.him188.ani.app.ui.subject.details.components
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -39,14 +39,21 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImagePainter
import me.him188.ani.app.data.models.subject.SubjectInfo
+import me.him188.ani.app.data.models.subject.fullDisplayName
import me.him188.ani.app.platform.currentAniBuildConfig
import me.him188.ani.app.ui.foundation.AsyncImage
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
import me.him188.ani.app.ui.foundation.layout.paddingIfNotEmpty
+import me.him188.ani.app.ui.foundation.widgets.LocalToaster
+import me.him188.ani.app.ui.lang.Lang
+import me.him188.ani.app.ui.lang.subject_title_copied
+import org.jetbrains.compose.resources.stringResource
const val COVER_WIDTH_TO_HEIGHT_RATIO = 849 / 1200f
@@ -66,6 +73,7 @@ internal fun SubjectDetailsHeader(
if (currentWindowAdaptiveInfo1().isWidthAtLeastMedium) {
SubjectDetailsHeaderWide(
coverImageUrl = coverImageUrl,
+ info = info,
title = {
Text(
info?.displayName ?: "",
@@ -90,6 +98,7 @@ internal fun SubjectDetailsHeader(
} else {
SubjectDetailsHeaderCompact(
coverImageUrl = coverImageUrl,
+ info = info,
title = { Text(info?.displayName ?: "") },
subtitle = { Text(info?.name ?: "") },
seasonTags = { seasonTags() },
@@ -108,6 +117,7 @@ internal fun SubjectDetailsHeader(
@Composable
fun SubjectDetailsHeaderCompact(
coverImageUrl: String?,
+ info: SubjectInfo?,
title: @Composable () -> Unit,
subtitle: @Composable () -> Unit,
seasonTags: @Composable () -> Unit,
@@ -143,8 +153,19 @@ fun SubjectDetailsHeaderCompact(
Modifier.fillMaxWidth(), // required by Rating
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
+ val clipboard = LocalClipboardManager.current
+ val toaster = LocalToaster.current
+ val copied = stringResource(Lang.subject_title_copied)
+ val fullName = info?.fullDisplayName.orEmpty()
+ val copy = {
+ if (fullName.isNotBlank()) {
+ clipboard.setText(AnnotatedString(fullName))
+ toaster.toast(copied)
+ }
+ }
+
var showSubtitle by remember { mutableStateOf(false) }
- Box(Modifier.clickable { showSubtitle = !showSubtitle }) {
+ Box(Modifier.combinedClickable(onClick = copy, onLongClick = { showSubtitle = !showSubtitle })) {
ProvideTextStyle(MaterialTheme.typography.titleLarge) {
if (showSubtitle) {
subtitle()
@@ -192,6 +213,7 @@ fun SubjectDetailsHeaderCompact(
@Composable
fun SubjectDetailsHeaderWide(
coverImageUrl: String?,
+ info: SubjectInfo?,
title: @Composable () -> Unit,
seasonTags: @Composable RowScope.() -> Unit,
collectionData: @Composable () -> Unit,
@@ -230,7 +252,18 @@ fun SubjectDetailsHeaderWide(
Modifier.weight(1f, fill = true),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
- Box(Modifier) {
+ val clipboard = LocalClipboardManager.current
+ val toaster = LocalToaster.current
+ val copied = stringResource(Lang.subject_title_copied)
+ val fullName = info?.fullDisplayName.orEmpty()
+ val copy = {
+ if (fullName.isNotBlank()) {
+ clipboard.setText(AnnotatedString(fullName))
+ toaster.toast(copied)
+ }
+ }
+
+ Box(Modifier.combinedClickable(onClick = copy, onLongClick = { })) {
ProvideTextStyle(MaterialTheme.typography.titleLarge) {
SelectionContainer {
title()