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()