fix wooden fish author and title display
This commit is contained in:
@@ -17,6 +17,7 @@ export type AuthUser = {
|
|||||||
export type PublicUserSummary = {
|
export type PublicUserSummary = {
|
||||||
id: string;
|
id: string;
|
||||||
publicUserCode: string;
|
publicUserCode: string;
|
||||||
|
username: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
avatarUrl: string | null;
|
avatarUrl: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub fn map_public_user_summary_payload(user: AuthUser) -> PublicUserSummaryPaylo
|
|||||||
PublicUserSummaryPayload {
|
PublicUserSummaryPayload {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
public_user_code: user.public_user_code,
|
public_user_code: user.public_user_code,
|
||||||
|
username: user.username,
|
||||||
display_name: user.display_name,
|
display_name: user.display_name,
|
||||||
avatar_url: user.avatar_url,
|
avatar_url: user.avatar_url,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const DEFAULT_HIT_OBJECT_REFERENCE_BYTES: &[u8] = include_bytes!(concat!(
|
|||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/../../../public/wooden-fish/default-hit-object.png"
|
"/../../../public/wooden-fish/default-hit-object.png"
|
||||||
));
|
));
|
||||||
|
const WOODEN_FISH_AUTHOR_FALLBACK_DISPLAY_NAME: &str = "玩家";
|
||||||
|
|
||||||
pub async fn create_wooden_fish_session(
|
pub async fn create_wooden_fish_session(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -80,7 +81,7 @@ pub async fn create_wooden_fish_session(
|
|||||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||||
let session_id = build_prefixed_uuid_id("wooden-fish-session-");
|
let session_id = build_prefixed_uuid_id("wooden-fish-session-");
|
||||||
let now = current_utc_micros();
|
let now = current_utc_micros();
|
||||||
let draft = build_wooden_fish_draft(&payload);
|
let draft = build_wooden_fish_draft(&payload, &state).await?;
|
||||||
let session = WoodenFishSessionSnapshotResponse {
|
let session = WoodenFishSessionSnapshotResponse {
|
||||||
session_id,
|
session_id,
|
||||||
owner_user_id,
|
owner_user_id,
|
||||||
@@ -145,6 +146,7 @@ pub async fn execute_wooden_fish_action(
|
|||||||
let Json(mut payload) =
|
let Json(mut payload) =
|
||||||
wooden_fish_json(payload, &request_context, WOODEN_FISH_CREATION_PROVIDER)?;
|
wooden_fish_json(payload, &request_context, WOODEN_FISH_CREATION_PROVIDER)?;
|
||||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||||
|
let author_display_name = resolve_author_display_name(&state, &authenticated);
|
||||||
maybe_generate_hit_object_asset(
|
maybe_generate_hit_object_asset(
|
||||||
&state,
|
&state,
|
||||||
&request_context,
|
&request_context,
|
||||||
@@ -156,7 +158,7 @@ pub async fn execute_wooden_fish_action(
|
|||||||
maybe_generate_hit_sound_asset(&mut payload);
|
maybe_generate_hit_sound_asset(&mut payload);
|
||||||
let response = state
|
let response = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.execute_wooden_fish_action(session_id, owner_user_id, payload)
|
.execute_wooden_fish_action(session_id, owner_user_id, author_display_name, payload)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
wooden_fish_error_response(
|
wooden_fish_error_response(
|
||||||
@@ -366,12 +368,20 @@ pub async fn get_wooden_fish_gallery_detail(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_wooden_fish_draft(payload: &WoodenFishWorkspaceCreateRequest) -> WoodenFishDraftResponse {
|
async fn build_wooden_fish_draft(
|
||||||
WoodenFishDraftResponse {
|
payload: &WoodenFishWorkspaceCreateRequest,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Result<WoodenFishDraftResponse, Response> {
|
||||||
|
Ok(WoodenFishDraftResponse {
|
||||||
template_id: WOODEN_FISH_TEMPLATE_ID.to_string(),
|
template_id: WOODEN_FISH_TEMPLATE_ID.to_string(),
|
||||||
template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(),
|
template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(),
|
||||||
profile_id: None,
|
profile_id: None,
|
||||||
work_title: payload.work_title.trim().to_string(),
|
work_title: resolve_wooden_fish_work_title(
|
||||||
|
state,
|
||||||
|
&payload.work_description,
|
||||||
|
&payload.hit_object_prompt,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
work_description: payload.work_description.trim().to_string(),
|
work_description: payload.work_description.trim().to_string(),
|
||||||
theme_tags: normalize_tags(payload.theme_tags.clone()),
|
theme_tags: normalize_tags(payload.theme_tags.clone()),
|
||||||
hit_object_prompt: clean_string(&payload.hit_object_prompt, DEFAULT_HIT_OBJECT_PROMPT),
|
hit_object_prompt: clean_string(&payload.hit_object_prompt, DEFAULT_HIT_OBJECT_PROMPT),
|
||||||
@@ -391,14 +401,13 @@ fn build_wooden_fish_draft(payload: &WoodenFishWorkspaceCreateRequest) -> Wooden
|
|||||||
.or_else(|| Some(default_wooden_fish_hit_sound_asset())),
|
.or_else(|| Some(default_wooden_fish_hit_sound_asset())),
|
||||||
cover_image_src: None,
|
cover_image_src: None,
|
||||||
generation_status: WoodenFishGenerationStatus::Draft,
|
generation_status: WoodenFishGenerationStatus::Draft,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_workspace_request(
|
fn validate_workspace_request(
|
||||||
request_context: &RequestContext,
|
request_context: &RequestContext,
|
||||||
payload: &WoodenFishWorkspaceCreateRequest,
|
payload: &WoodenFishWorkspaceCreateRequest,
|
||||||
) -> Result<(), Response> {
|
) -> Result<(), Response> {
|
||||||
ensure_non_empty(request_context, &payload.work_title, "workTitle")?;
|
|
||||||
if payload.template_id.trim() != WOODEN_FISH_TEMPLATE_ID {
|
if payload.template_id.trim() != WOODEN_FISH_TEMPLATE_ID {
|
||||||
return Err(wooden_fish_error_response(
|
return Err(wooden_fish_error_response(
|
||||||
request_context,
|
request_context,
|
||||||
@@ -412,6 +421,77 @@ fn validate_workspace_request(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_author_display_name(
|
||||||
|
state: &AppState,
|
||||||
|
authenticated: &AuthenticatedAccessToken,
|
||||||
|
) -> String {
|
||||||
|
state
|
||||||
|
.auth_user_service()
|
||||||
|
.get_user_by_id(authenticated.claims().user_id())
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|user| user.display_name)
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| WOODEN_FISH_AUTHOR_FALLBACK_DISPLAY_NAME.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_wooden_fish_work_title(
|
||||||
|
state: &AppState,
|
||||||
|
work_description: &str,
|
||||||
|
hit_object_prompt: &str,
|
||||||
|
) -> Result<String, Response> {
|
||||||
|
let description = clean_string(work_description, hit_object_prompt);
|
||||||
|
if description.is_empty() {
|
||||||
|
return Ok(WOODEN_FISH_TEMPLATE_NAME.to_string());
|
||||||
|
}
|
||||||
|
let Some(llm_client) = state.llm_client() else {
|
||||||
|
return Ok(WOODEN_FISH_TEMPLATE_NAME.to_string());
|
||||||
|
};
|
||||||
|
let request = platform_llm::LlmTextRequest::new(vec![
|
||||||
|
platform_llm::LlmMessage::system(
|
||||||
|
"你是中文作品标题编辑。请根据敲木鱼作品描述生成一个适合卡片展示的简短中文标题,只输出纯文本,不要 JSON、标点解释或引号。",
|
||||||
|
),
|
||||||
|
platform_llm::LlmMessage::user(format!(
|
||||||
|
"作品描述:{description}\n\n请生成 2 到 8 个中文字符为主的标题。"
|
||||||
|
)),
|
||||||
|
])
|
||||||
|
.with_model(crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL)
|
||||||
|
.with_responses_api();
|
||||||
|
let response = llm_client.request_text(request).await;
|
||||||
|
match response {
|
||||||
|
Ok(response) => {
|
||||||
|
let title = normalize_wooden_fish_generated_work_title(response.content.as_str());
|
||||||
|
if title.is_empty() {
|
||||||
|
Ok(WOODEN_FISH_TEMPLATE_NAME.to_string())
|
||||||
|
} else {
|
||||||
|
Ok(title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Ok(WOODEN_FISH_TEMPLATE_NAME.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_wooden_fish_generated_work_title(value: &str) -> String {
|
||||||
|
let normalized = value
|
||||||
|
.trim()
|
||||||
|
.trim_matches(|ch: char| {
|
||||||
|
ch.is_ascii_punctuation()
|
||||||
|
|| matches!(
|
||||||
|
ch,
|
||||||
|
',' | '。' | '、' | ';' | ':' | '!' | '?' | '“' | '”' | '《' | '》'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.chars()
|
||||||
|
.filter(|ch| !ch.is_control())
|
||||||
|
.collect::<String>();
|
||||||
|
let chars = normalized.chars().collect::<Vec<_>>();
|
||||||
|
if chars.len() <= 8 {
|
||||||
|
normalized
|
||||||
|
} else {
|
||||||
|
chars.into_iter().take(8).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn maybe_generate_hit_object_asset(
|
async fn maybe_generate_hit_object_asset(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
request_context: &RequestContext,
|
request_context: &RequestContext,
|
||||||
@@ -585,7 +665,10 @@ async fn generate_wooden_fish_image_assets(
|
|||||||
prompt: &str,
|
prompt: &str,
|
||||||
hit_object_reference_image_src: Option<&str>,
|
hit_object_reference_image_src: Option<&str>,
|
||||||
) -> Result<WoodenFishGeneratedImageAssets, AppError> {
|
) -> Result<WoodenFishGeneratedImageAssets, AppError> {
|
||||||
let settings = require_openai_image_settings(state)?;
|
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
|
||||||
|
Some(owner_user_id.to_string()),
|
||||||
|
Some(profile_id.to_string()),
|
||||||
|
);
|
||||||
let http_client = build_openai_image_http_client(&settings)?;
|
let http_client = build_openai_image_http_client(&settings)?;
|
||||||
let clean_reference_image_src = hit_object_reference_image_src
|
let clean_reference_image_src = hit_object_reference_image_src
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub struct AuthUserPayload {
|
|||||||
pub struct PublicUserSummaryPayload {
|
pub struct PublicUserSummaryPayload {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub public_user_code: String,
|
pub public_user_code: String,
|
||||||
|
pub username: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub avatar_url: Option<String>,
|
pub avatar_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ impl SpacetimeClient {
|
|||||||
&self,
|
&self,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
owner_user_id: String,
|
owner_user_id: String,
|
||||||
|
author_display_name: String,
|
||||||
payload: WoodenFishActionRequest,
|
payload: WoodenFishActionRequest,
|
||||||
) -> Result<WoodenFishActionResponse, SpacetimeClientError> {
|
) -> Result<WoodenFishActionResponse, SpacetimeClientError> {
|
||||||
let current = self
|
let current = self
|
||||||
@@ -93,6 +94,7 @@ impl SpacetimeClient {
|
|||||||
let (procedure, _) = build_wooden_fish_action_plan(
|
let (procedure, _) = build_wooden_fish_action_plan(
|
||||||
¤t,
|
¤t,
|
||||||
&owner_user_id,
|
&owner_user_id,
|
||||||
|
&author_display_name,
|
||||||
&payload,
|
&payload,
|
||||||
current_unix_micros(),
|
current_unix_micros(),
|
||||||
)?;
|
)?;
|
||||||
@@ -416,6 +418,7 @@ enum WoodenFishAssetRefresh {
|
|||||||
fn build_wooden_fish_action_plan(
|
fn build_wooden_fish_action_plan(
|
||||||
current: &WoodenFishSessionSnapshotResponse,
|
current: &WoodenFishSessionSnapshotResponse,
|
||||||
owner_user_id: &str,
|
owner_user_id: &str,
|
||||||
|
author_display_name: &str,
|
||||||
payload: &WoodenFishActionRequest,
|
payload: &WoodenFishActionRequest,
|
||||||
now_micros: i64,
|
now_micros: i64,
|
||||||
) -> Result<(WoodenFishActionProcedure, WoodenFishDraftResponse), SpacetimeClientError> {
|
) -> Result<(WoodenFishActionProcedure, WoodenFishDraftResponse), SpacetimeClientError> {
|
||||||
@@ -440,6 +443,7 @@ fn build_wooden_fish_action_plan(
|
|||||||
WoodenFishActionProcedure::Compile(build_compile_input(
|
WoodenFishActionProcedure::Compile(build_compile_input(
|
||||||
current,
|
current,
|
||||||
owner_user_id,
|
owner_user_id,
|
||||||
|
author_display_name,
|
||||||
&profile_id,
|
&profile_id,
|
||||||
&mut draft,
|
&mut draft,
|
||||||
WoodenFishAssetRefresh::Preserve,
|
WoodenFishAssetRefresh::Preserve,
|
||||||
@@ -450,6 +454,7 @@ fn build_wooden_fish_action_plan(
|
|||||||
WoodenFishActionProcedure::Compile(build_compile_input(
|
WoodenFishActionProcedure::Compile(build_compile_input(
|
||||||
current,
|
current,
|
||||||
owner_user_id,
|
owner_user_id,
|
||||||
|
author_display_name,
|
||||||
&profile_id,
|
&profile_id,
|
||||||
&mut draft,
|
&mut draft,
|
||||||
WoodenFishAssetRefresh::HitObject,
|
WoodenFishAssetRefresh::HitObject,
|
||||||
@@ -460,6 +465,7 @@ fn build_wooden_fish_action_plan(
|
|||||||
WoodenFishActionProcedure::Compile(build_compile_input(
|
WoodenFishActionProcedure::Compile(build_compile_input(
|
||||||
current,
|
current,
|
||||||
owner_user_id,
|
owner_user_id,
|
||||||
|
author_display_name,
|
||||||
&profile_id,
|
&profile_id,
|
||||||
&mut draft,
|
&mut draft,
|
||||||
WoodenFishAssetRefresh::HitSound,
|
WoodenFishAssetRefresh::HitSound,
|
||||||
@@ -577,6 +583,7 @@ fn merge_action_into_draft(
|
|||||||
fn build_compile_input(
|
fn build_compile_input(
|
||||||
current: &WoodenFishSessionSnapshotResponse,
|
current: &WoodenFishSessionSnapshotResponse,
|
||||||
owner_user_id: &str,
|
owner_user_id: &str,
|
||||||
|
author_display_name: &str,
|
||||||
profile_id: &str,
|
profile_id: &str,
|
||||||
draft: &mut WoodenFishDraftResponse,
|
draft: &mut WoodenFishDraftResponse,
|
||||||
refresh: WoodenFishAssetRefresh,
|
refresh: WoodenFishAssetRefresh,
|
||||||
@@ -611,7 +618,7 @@ fn build_compile_input(
|
|||||||
session_id: current.session_id.clone(),
|
session_id: current.session_id.clone(),
|
||||||
owner_user_id: owner_user_id.to_string(),
|
owner_user_id: owner_user_id.to_string(),
|
||||||
profile_id: profile_id.to_string(),
|
profile_id: profile_id.to_string(),
|
||||||
author_display_name: "敲木鱼玩家".to_string(),
|
author_display_name: author_display_name.trim().to_string(),
|
||||||
work_title: draft.work_title.clone(),
|
work_title: draft.work_title.clone(),
|
||||||
work_description: draft.work_description.clone(),
|
work_description: draft.work_description.clone(),
|
||||||
theme_tags_json: Some(json_string(&draft.theme_tags)?),
|
theme_tags_json: Some(json_string(&draft.theme_tags)?),
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ const authServiceMocks = vi.hoisted(() => ({
|
|||||||
async (publicUserCode: string): Promise<PublicUserSummary> => ({
|
async (publicUserCode: string): Promise<PublicUserSummary> => ({
|
||||||
id: `public-user-${publicUserCode}`,
|
id: `public-user-${publicUserCode}`,
|
||||||
publicUserCode,
|
publicUserCode,
|
||||||
|
username: 'author_user',
|
||||||
displayName: '公开作者',
|
displayName: '公开作者',
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
}),
|
}),
|
||||||
@@ -195,6 +196,7 @@ const authServiceMocks = vi.hoisted(() => ({
|
|||||||
async (userId: string): Promise<PublicUserSummary> => ({
|
async (userId: string): Promise<PublicUserSummary> => ({
|
||||||
id: userId,
|
id: userId,
|
||||||
publicUserCode: `code-${userId}`,
|
publicUserCode: `code-${userId}`,
|
||||||
|
username: 'author_user',
|
||||||
displayName: '公开作者',
|
displayName: '公开作者',
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -328,6 +328,7 @@ const {
|
|||||||
async (code: string): Promise<PublicUserSummary> => ({
|
async (code: string): Promise<PublicUserSummary> => ({
|
||||||
id: `id-${code}`,
|
id: `id-${code}`,
|
||||||
publicUserCode: code,
|
publicUserCode: code,
|
||||||
|
username: 'author_user',
|
||||||
displayName: '公开作者',
|
displayName: '公开作者',
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
}),
|
}),
|
||||||
@@ -336,6 +337,7 @@ const {
|
|||||||
async (userId: string): Promise<PublicUserSummary> => ({
|
async (userId: string): Promise<PublicUserSummary> => ({
|
||||||
id: userId,
|
id: userId,
|
||||||
publicUserCode: `code-${userId}`,
|
publicUserCode: `code-${userId}`,
|
||||||
|
username: 'author_user',
|
||||||
displayName: '公开作者',
|
displayName: '公开作者',
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
}),
|
}),
|
||||||
@@ -3285,6 +3287,7 @@ test('mobile recommend meta loads real author avatar from public user summary',
|
|||||||
mockGetPublicAuthUserById.mockResolvedValueOnce({
|
mockGetPublicAuthUserById.mockResolvedValueOnce({
|
||||||
id: 'user-2',
|
id: 'user-2',
|
||||||
publicUserCode: 'SY-00000002',
|
publicUserCode: 'SY-00000002',
|
||||||
|
username: 'puzzle_user',
|
||||||
displayName: '拼图玩家',
|
displayName: '拼图玩家',
|
||||||
avatarUrl: 'data:image/png;base64,AUTHOR',
|
avatarUrl: 'data:image/png;base64,AUTHOR',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -611,6 +611,7 @@ function WorldCard({
|
|||||||
onClick,
|
onClick,
|
||||||
className,
|
className,
|
||||||
authorAvatarUrl,
|
authorAvatarUrl,
|
||||||
|
authorUsername,
|
||||||
feedCardKey,
|
feedCardKey,
|
||||||
enableCoverCarousel = false,
|
enableCoverCarousel = false,
|
||||||
isCoverCarouselActive = false,
|
isCoverCarouselActive = false,
|
||||||
@@ -620,6 +621,7 @@ function WorldCard({
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
authorAvatarUrl?: string | null;
|
authorAvatarUrl?: string | null;
|
||||||
|
authorUsername?: string | null;
|
||||||
feedCardKey?: string;
|
feedCardKey?: string;
|
||||||
enableCoverCarousel?: boolean;
|
enableCoverCarousel?: boolean;
|
||||||
isCoverCarouselActive?: boolean;
|
isCoverCarouselActive?: boolean;
|
||||||
@@ -653,7 +655,10 @@ function WorldCard({
|
|||||||
const remixCount = getPlatformWorldRemixCount(entry);
|
const remixCount = getPlatformWorldRemixCount(entry);
|
||||||
const likeCount = getPlatformWorldLikeCount(entry);
|
const likeCount = getPlatformWorldLikeCount(entry);
|
||||||
const typeLabel = describePublicGalleryCardKind(entry);
|
const typeLabel = describePublicGalleryCardKind(entry);
|
||||||
const authorName = entry.authorDisplayName.trim() || '玩家';
|
const authorName = resolvePublicEntryAuthorDisplayText(
|
||||||
|
entry,
|
||||||
|
authorUsername,
|
||||||
|
);
|
||||||
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
|
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
|
||||||
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
|
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
|
||||||
const cardLabel = `${entry.worldName},${typeLabel},${formatCompactCount(playCount)}游玩,${formatCompactCount(remixCount)}改造,${formatCompactCount(likeCount)}点赞`;
|
const cardLabel = `${entry.worldName},${typeLabel},${formatCompactCount(playCount)}游玩,${formatCompactCount(remixCount)}改造,${formatCompactCount(likeCount)}点赞`;
|
||||||
@@ -935,6 +940,7 @@ function RecommendRuntimePreviewCard({
|
|||||||
function RecommendSwipeCard({
|
function RecommendSwipeCard({
|
||||||
entry,
|
entry,
|
||||||
authorAvatarUrl,
|
authorAvatarUrl,
|
||||||
|
authorUsername,
|
||||||
isActive,
|
isActive,
|
||||||
visual,
|
visual,
|
||||||
shareState,
|
shareState,
|
||||||
@@ -948,6 +954,7 @@ function RecommendSwipeCard({
|
|||||||
}: {
|
}: {
|
||||||
entry: PlatformPublicGalleryCard;
|
entry: PlatformPublicGalleryCard;
|
||||||
authorAvatarUrl?: string | null;
|
authorAvatarUrl?: string | null;
|
||||||
|
authorUsername?: string | null;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
visual: ReactNode;
|
visual: ReactNode;
|
||||||
shareState?: 'idle' | 'copied' | 'failed';
|
shareState?: 'idle' | 'copied' | 'failed';
|
||||||
@@ -972,6 +979,7 @@ function RecommendSwipeCard({
|
|||||||
<RecommendRuntimeMeta
|
<RecommendRuntimeMeta
|
||||||
entry={entry}
|
entry={entry}
|
||||||
authorAvatarUrl={authorAvatarUrl}
|
authorAvatarUrl={authorAvatarUrl}
|
||||||
|
authorUsername={authorUsername}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
shareState={shareState}
|
shareState={shareState}
|
||||||
onDragPointerDown={onDragPointerDown}
|
onDragPointerDown={onDragPointerDown}
|
||||||
@@ -990,6 +998,7 @@ function RecommendSwipeCard({
|
|||||||
function RecommendRuntimeMeta({
|
function RecommendRuntimeMeta({
|
||||||
entry,
|
entry,
|
||||||
authorAvatarUrl,
|
authorAvatarUrl,
|
||||||
|
authorUsername,
|
||||||
onDragPointerDown,
|
onDragPointerDown,
|
||||||
onDragPointerMove,
|
onDragPointerMove,
|
||||||
onDragPointerUp,
|
onDragPointerUp,
|
||||||
@@ -1002,6 +1011,7 @@ function RecommendRuntimeMeta({
|
|||||||
}: {
|
}: {
|
||||||
entry: PlatformPublicGalleryCard;
|
entry: PlatformPublicGalleryCard;
|
||||||
authorAvatarUrl?: string | null;
|
authorAvatarUrl?: string | null;
|
||||||
|
authorUsername?: string | null;
|
||||||
onDragPointerDown?: (event: PointerEvent<HTMLElement>) => void;
|
onDragPointerDown?: (event: PointerEvent<HTMLElement>) => void;
|
||||||
onDragPointerMove?: (event: PointerEvent<HTMLElement>) => void;
|
onDragPointerMove?: (event: PointerEvent<HTMLElement>) => void;
|
||||||
onDragPointerUp?: (event: PointerEvent<HTMLElement>) => void;
|
onDragPointerUp?: (event: PointerEvent<HTMLElement>) => void;
|
||||||
@@ -1014,7 +1024,10 @@ function RecommendRuntimeMeta({
|
|||||||
}) {
|
}) {
|
||||||
const likeCount = getPlatformWorldLikeCount(entry);
|
const likeCount = getPlatformWorldLikeCount(entry);
|
||||||
const remixCount = getPlatformWorldRemixCount(entry);
|
const remixCount = getPlatformWorldRemixCount(entry);
|
||||||
const authorName = entry.authorDisplayName.trim() || '玩家';
|
const authorName = resolvePublicEntryAuthorDisplayText(
|
||||||
|
entry,
|
||||||
|
authorUsername,
|
||||||
|
);
|
||||||
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
|
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
|
||||||
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
|
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
|
||||||
const displayName = formatPlatformWorkDisplayName(entry.worldName);
|
const displayName = formatPlatformWorkDisplayName(entry.worldName);
|
||||||
@@ -1890,28 +1903,28 @@ async function getPublicWorkAuthorSummary(
|
|||||||
|
|
||||||
function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
|
function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
|
||||||
if (isBigFishGalleryEntry(entry)) {
|
if (isBigFishGalleryEntry(entry)) {
|
||||||
return formatPlatformWorkDisplayTag('??');
|
return formatPlatformWorkDisplayTag('大鱼吃小鱼');
|
||||||
}
|
}
|
||||||
if (isPuzzleGalleryEntry(entry)) {
|
if (isPuzzleGalleryEntry(entry)) {
|
||||||
return formatPlatformWorkDisplayTag('??');
|
return formatPlatformWorkDisplayTag('拼图');
|
||||||
}
|
}
|
||||||
if (isMatch3DGalleryEntry(entry)) {
|
if (isMatch3DGalleryEntry(entry)) {
|
||||||
return formatPlatformWorkDisplayTag('??');
|
return formatPlatformWorkDisplayTag('抓大鹅');
|
||||||
}
|
}
|
||||||
if (isSquareHoleGalleryEntry(entry)) {
|
if (isSquareHoleGalleryEntry(entry)) {
|
||||||
return formatPlatformWorkDisplayTag('??');
|
return formatPlatformWorkDisplayTag('方洞挑战');
|
||||||
}
|
}
|
||||||
if (isJumpHopGalleryEntry(entry)) {
|
if (isJumpHopGalleryEntry(entry)) {
|
||||||
return formatPlatformWorkDisplayTag('???');
|
return formatPlatformWorkDisplayTag('跳一跳');
|
||||||
}
|
}
|
||||||
if (isWoodenFishGalleryEntry(entry)) {
|
if (isWoodenFishGalleryEntry(entry)) {
|
||||||
return formatPlatformWorkDisplayTag('???');
|
return formatPlatformWorkDisplayTag('敲木鱼');
|
||||||
}
|
}
|
||||||
if (isVisualNovelGalleryEntry(entry)) {
|
if (isVisualNovelGalleryEntry(entry)) {
|
||||||
return formatPlatformWorkDisplayTag('??');
|
return formatPlatformWorkDisplayTag('视觉小说');
|
||||||
}
|
}
|
||||||
if (isBarkBattleGalleryEntry(entry)) {
|
if (isBarkBattleGalleryEntry(entry)) {
|
||||||
return formatPlatformWorkDisplayTag('??');
|
return formatPlatformWorkDisplayTag('汪汪声浪');
|
||||||
}
|
}
|
||||||
if (isEdutainmentGalleryEntry(entry)) {
|
if (isEdutainmentGalleryEntry(entry)) {
|
||||||
return formatPlatformWorkDisplayTag(entry.templateName);
|
return formatPlatformWorkDisplayTag(entry.templateName);
|
||||||
@@ -1922,6 +1935,17 @@ function getPublicAuthorAvatarLabel(authorDisplayName: string) {
|
|||||||
return Array.from(authorDisplayName.trim() || '玩')[0] ?? '玩';
|
return Array.from(authorDisplayName.trim() || '玩')[0] ?? '玩';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePublicEntryAuthorDisplayText(
|
||||||
|
entry: PlatformPublicGalleryCard,
|
||||||
|
authorUsername?: string | null,
|
||||||
|
) {
|
||||||
|
if (isWoodenFishGalleryEntry(entry)) {
|
||||||
|
return authorUsername?.trim() || entry.authorDisplayName.trim() || '玩家';
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.authorDisplayName.trim() || '玩家';
|
||||||
|
}
|
||||||
|
|
||||||
function getPlatformWorldLikeCount(entry: PlatformWorldCardLike) {
|
function getPlatformWorldLikeCount(entry: PlatformWorldCardLike) {
|
||||||
return Math.max(0, Math.round(entry.likeCount ?? 0));
|
return Math.max(0, Math.round(entry.likeCount ?? 0));
|
||||||
}
|
}
|
||||||
@@ -4178,6 +4202,17 @@ export function RpgEntryHomeView({
|
|||||||
},
|
},
|
||||||
[publicAuthorSummariesByKey],
|
[publicAuthorSummariesByKey],
|
||||||
);
|
);
|
||||||
|
const getPublicEntryAuthorUsername = useCallback(
|
||||||
|
(entry: PlatformPublicGalleryCard) => {
|
||||||
|
const authorLookupKey = buildPublicWorkAuthorLookupKey(entry);
|
||||||
|
if (!authorLookupKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicAuthorSummariesByKey[authorLookupKey]?.username?.trim() || null;
|
||||||
|
},
|
||||||
|
[publicAuthorSummariesByKey],
|
||||||
|
);
|
||||||
const activeCategoryGroup =
|
const activeCategoryGroup =
|
||||||
categoryGroups.find((group) => group.tag === selectedCategoryTag) ??
|
categoryGroups.find((group) => group.tag === selectedCategoryTag) ??
|
||||||
categoryGroups[0] ??
|
categoryGroups[0] ??
|
||||||
@@ -5523,6 +5558,9 @@ export function RpgEntryHomeView({
|
|||||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
||||||
previousRecommendEntry,
|
previousRecommendEntry,
|
||||||
)}
|
)}
|
||||||
|
authorUsername={getPublicEntryAuthorUsername(
|
||||||
|
previousRecommendEntry,
|
||||||
|
)}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
visual={
|
visual={
|
||||||
<RecommendRuntimePreviewCard
|
<RecommendRuntimePreviewCard
|
||||||
@@ -5540,6 +5578,9 @@ export function RpgEntryHomeView({
|
|||||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
||||||
activeRecommendEntry,
|
activeRecommendEntry,
|
||||||
)}
|
)}
|
||||||
|
authorUsername={getPublicEntryAuthorUsername(
|
||||||
|
activeRecommendEntry,
|
||||||
|
)}
|
||||||
isActive
|
isActive
|
||||||
visual={
|
visual={
|
||||||
<div className="platform-recommend-runtime-viewport">
|
<div className="platform-recommend-runtime-viewport">
|
||||||
@@ -5564,6 +5605,9 @@ export function RpgEntryHomeView({
|
|||||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
||||||
nextRecommendEntry,
|
nextRecommendEntry,
|
||||||
)}
|
)}
|
||||||
|
authorUsername={getPublicEntryAuthorUsername(
|
||||||
|
nextRecommendEntry,
|
||||||
|
)}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
visual={
|
visual={
|
||||||
<RecommendRuntimePreviewCard
|
<RecommendRuntimePreviewCard
|
||||||
@@ -5717,6 +5761,7 @@ export function RpgEntryHomeView({
|
|||||||
onClick={() => onOpenGalleryDetail(entry)}
|
onClick={() => onOpenGalleryDetail(entry)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||||
|
authorUsername={getPublicEntryAuthorUsername(entry)}
|
||||||
feedCardKey={cardKey}
|
feedCardKey={cardKey}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -5782,6 +5827,7 @@ export function RpgEntryHomeView({
|
|||||||
onClick={() => onOpenGalleryDetail(entry)}
|
onClick={() => onOpenGalleryDetail(entry)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||||
|
authorUsername={getPublicEntryAuthorUsername(entry)}
|
||||||
feedCardKey={cardKey}
|
feedCardKey={cardKey}
|
||||||
enableCoverCarousel={mobileFeedCarouselEnabled}
|
enableCoverCarousel={mobileFeedCarouselEnabled}
|
||||||
isCoverCarouselActive={
|
isCoverCarouselActive={
|
||||||
@@ -5888,6 +5934,7 @@ export function RpgEntryHomeView({
|
|||||||
onClick={() => openRecommendGalleryDetail(entry)}
|
onClick={() => openRecommendGalleryDetail(entry)}
|
||||||
className="w-full min-w-0"
|
className="w-full min-w-0"
|
||||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||||
|
authorUsername={getPublicEntryAuthorUsername(entry)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -5915,6 +5962,7 @@ export function RpgEntryHomeView({
|
|||||||
onClick={() => openRecommendGalleryDetail(entry)}
|
onClick={() => openRecommendGalleryDetail(entry)}
|
||||||
className="w-full min-w-0"
|
className="w-full min-w-0"
|
||||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||||
|
authorUsername={getPublicEntryAuthorUsername(entry)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{onOpenChildMotionDemo ? (
|
{onOpenChildMotionDemo ? (
|
||||||
@@ -5975,6 +6023,7 @@ export function RpgEntryHomeView({
|
|||||||
onClick={() => openRecommendGalleryDetail(entry)}
|
onClick={() => openRecommendGalleryDetail(entry)}
|
||||||
className="w-full min-w-0"
|
className="w-full min-w-0"
|
||||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||||
|
authorUsername={getPublicEntryAuthorUsername(entry)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -6490,6 +6539,7 @@ export function RpgEntryHomeView({
|
|||||||
onClick={() => openRecommendGalleryDetail(entry)}
|
onClick={() => openRecommendGalleryDetail(entry)}
|
||||||
className="w-full min-w-0"
|
className="w-full min-w-0"
|
||||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||||
|
authorUsername={getPublicEntryAuthorUsername(entry)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -6660,6 +6710,7 @@ export function RpgEntryHomeView({
|
|||||||
onClick={() => openRecommendGalleryDetail(entry)}
|
onClick={() => openRecommendGalleryDetail(entry)}
|
||||||
className="w-full min-w-0"
|
className="w-full min-w-0"
|
||||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||||
|
authorUsername={getPublicEntryAuthorUsername(entry)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ export type PlatformWoodenFishGalleryCard = {
|
|||||||
sourceSessionId?: string | null;
|
sourceSessionId?: string | null;
|
||||||
publicWorkCode: string;
|
publicWorkCode: string;
|
||||||
ownerUserId: string;
|
ownerUserId: string;
|
||||||
|
authorUsername?: string | null;
|
||||||
authorDisplayName: string;
|
authorDisplayName: string;
|
||||||
worldName: string;
|
worldName: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
@@ -562,6 +563,10 @@ export function mapWoodenFishWorkToPlatformGalleryCard(
|
|||||||
? summary.publicWorkCode
|
? summary.publicWorkCode
|
||||||
: buildWoodenFishPublicWorkCode(summary.profileId),
|
: buildWoodenFishPublicWorkCode(summary.profileId),
|
||||||
ownerUserId: summary.ownerUserId,
|
ownerUserId: summary.ownerUserId,
|
||||||
|
authorUsername:
|
||||||
|
'authorUsername' in summary && typeof summary.authorUsername === 'string'
|
||||||
|
? summary.authorUsername
|
||||||
|
: null,
|
||||||
authorDisplayName:
|
authorDisplayName:
|
||||||
'authorDisplayName' in summary ? summary.authorDisplayName : '玩家',
|
'authorDisplayName' in summary ? summary.authorDisplayName : '玩家',
|
||||||
worldName: summary.workTitle,
|
worldName: summary.workTitle,
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ type WoodenFishWorkspaceFormState = {
|
|||||||
floatingWords: string[];
|
floatingWords: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_WORK_TITLE = '今日敲木鱼';
|
|
||||||
const DEFAULT_THEME_TAGS = ['敲木鱼', '解压'];
|
const DEFAULT_THEME_TAGS = ['敲木鱼', '解压'];
|
||||||
const DEFAULT_FLOATING_WORDS = ['幸运'];
|
const DEFAULT_FLOATING_WORDS = ['幸运'];
|
||||||
const MAX_FLOATING_WORD_COUNT = 8;
|
const MAX_FLOATING_WORD_COUNT = 8;
|
||||||
@@ -309,8 +308,9 @@ export function WoodenFishWorkspace({
|
|||||||
try {
|
try {
|
||||||
const payload: WoodenFishWorkspaceCreateRequest = {
|
const payload: WoodenFishWorkspaceCreateRequest = {
|
||||||
templateId: 'wooden-fish',
|
templateId: 'wooden-fish',
|
||||||
workTitle: DEFAULT_WORK_TITLE,
|
workTitle: '',
|
||||||
workDescription: '',
|
workDescription:
|
||||||
|
formState.hitObjectPrompt.trim() || WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT,
|
||||||
themeTags: DEFAULT_THEME_TAGS,
|
themeTags: DEFAULT_THEME_TAGS,
|
||||||
hitObjectPrompt:
|
hitObjectPrompt:
|
||||||
formState.hitObjectPrompt.trim() || WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT,
|
formState.hitObjectPrompt.trim() || WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT,
|
||||||
|
|||||||
Reference in New Issue
Block a user