feat(characters): use local character catalog
This commit is contained in:
@@ -3,9 +3,6 @@
|
||||
*
|
||||
*
|
||||
* `SizedBox.expand(child: Assets.images.picBgHome.image(fit: BoxFit.cover))`
|
||||
* 资源: /public/images/cover/elio.png
|
||||
* 原名: pic_bg_home.png (snake_case)
|
||||
*
|
||||
* sizes 属性说明:
|
||||
* 图片在 MobileShell 内(max-width: 540px)。
|
||||
* - 视口 ≤ 540px:MobileShell = 100vw,图片 = 100vw
|
||||
@@ -13,11 +10,15 @@
|
||||
*/
|
||||
import Image from "next/image";
|
||||
|
||||
export function SplashBackground() {
|
||||
export function SplashBackground({
|
||||
src = "/images/cover/elio.png",
|
||||
}: {
|
||||
src?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="absolute inset-0 z-0 overflow-hidden bg-sidebar-background">
|
||||
<Image
|
||||
src="/images/cover/elio.png"
|
||||
src={src}
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
|
||||
@@ -8,12 +8,16 @@ import styles from "./splash-latest-message.module.css";
|
||||
|
||||
export interface SplashLatestMessageProps {
|
||||
message: string | null;
|
||||
characterName: string;
|
||||
avatarUrl: string;
|
||||
isLoading?: boolean;
|
||||
onOpenChat: () => void;
|
||||
}
|
||||
|
||||
export function SplashLatestMessage({
|
||||
message,
|
||||
characterName,
|
||||
avatarUrl,
|
||||
isLoading = false,
|
||||
onOpenChat,
|
||||
}: SplashLatestMessageProps) {
|
||||
@@ -26,10 +30,11 @@ export function SplashLatestMessage({
|
||||
data-analytics-label="Open latest message"
|
||||
className={styles.card}
|
||||
onClick={onOpenChat}
|
||||
aria-label="Open latest message from Elio"
|
||||
aria-label={`Open latest message from ${characterName}`}
|
||||
>
|
||||
<span className={styles.avatarWrap}>
|
||||
<CharacterAvatar
|
||||
src={avatarUrl}
|
||||
alt=""
|
||||
size="100%"
|
||||
imageSize={42}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { LoginStatus } from "@/data/schemas/auth";
|
||||
import { DEFAULT_CHARACTER_ID } from "@/data/constants/character";
|
||||
import { useHasHydrated } from "@/hooks/use-has-hydrated";
|
||||
import { loadSplashLatestMessagePreview } from "@/lib/chat/splash_latest_message";
|
||||
import { useSplashLatestMessageCache } from "@/providers/splash-latest-message-provider";
|
||||
@@ -12,6 +13,7 @@ import { Result } from "@/utils/result";
|
||||
const log = new Logger("SplashLatestMessage");
|
||||
|
||||
export interface UseSplashLatestMessageInput {
|
||||
characterId?: string;
|
||||
hasInitialized: boolean;
|
||||
isAuthLoading: boolean;
|
||||
loginStatus: LoginStatus;
|
||||
@@ -29,6 +31,7 @@ interface SplashLatestMessageState {
|
||||
}
|
||||
|
||||
export function useSplashLatestMessage({
|
||||
characterId = DEFAULT_CHARACTER_ID,
|
||||
hasInitialized,
|
||||
isAuthLoading,
|
||||
loginStatus,
|
||||
@@ -45,7 +48,7 @@ export function useSplashLatestMessage({
|
||||
hasInitialized &&
|
||||
!isAuthLoading &&
|
||||
loginStatus !== "notLoggedIn";
|
||||
const requestKey = shouldLoad ? loginStatus : "";
|
||||
const requestKey = shouldLoad ? `${loginStatus}:${characterId}` : "";
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldLoad) return;
|
||||
@@ -53,7 +56,10 @@ export function useSplashLatestMessage({
|
||||
let cancelled = false;
|
||||
|
||||
const loadLatestMessage = async () => {
|
||||
const result = await loadSplashLatestMessagePreview({ cache });
|
||||
const result = await loadSplashLatestMessagePreview({
|
||||
cache,
|
||||
characterId,
|
||||
});
|
||||
if (cancelled) return;
|
||||
|
||||
if (Result.isErr(result)) {
|
||||
@@ -77,7 +83,7 @@ export function useSplashLatestMessage({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [cache, requestKey, shouldLoad]);
|
||||
}, [cache, characterId, requestKey, shouldLoad]);
|
||||
|
||||
if (!shouldLoad) return { message: null, isLoading: false };
|
||||
if (state.key !== requestKey || !state.loaded) {
|
||||
|
||||
+19
-3
@@ -1,5 +1,21 @@
|
||||
import { SplashScreen } from "@/app/splash/splash-screen";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function SplashPage() {
|
||||
return <SplashScreen />;
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
import {
|
||||
appendRouteSearchParams,
|
||||
getCharacterRoutes,
|
||||
type RouteSearchParams,
|
||||
} from "@/router/routes";
|
||||
|
||||
export default async function SplashPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<RouteSearchParams>;
|
||||
}) {
|
||||
redirect(
|
||||
appendRouteSearchParams(
|
||||
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).splash,
|
||||
await searchParams,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { AppBottomNav, MobileShell } from "@/app/_components/core";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||
import {
|
||||
useActiveCharacter,
|
||||
useActiveCharacterRoutes,
|
||||
} from "@/providers/character-provider";
|
||||
import { useAuthState } from "@/stores/auth/auth-context";
|
||||
import { pwaUtil } from "@/utils/pwa";
|
||||
|
||||
@@ -20,8 +23,11 @@ import styles from "./components/splash-screen.module.css";
|
||||
|
||||
export function SplashScreen() {
|
||||
const navigator = useAppNavigator();
|
||||
const character = useActiveCharacter();
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
const authState = useAuthState();
|
||||
const latestMessage = useSplashLatestMessage({
|
||||
characterId: character.id,
|
||||
hasInitialized: authState.hasInitialized,
|
||||
isAuthLoading: authState.isLoading,
|
||||
loginStatus: authState.loginStatus,
|
||||
@@ -32,11 +38,11 @@ export function SplashScreen() {
|
||||
};
|
||||
|
||||
const handleOpenPrivateRoom = () => {
|
||||
navigator.push(ROUTES.privateRoom, { scroll: false });
|
||||
navigator.push(characterRoutes.privateRoom, { scroll: false });
|
||||
};
|
||||
|
||||
const handleOpenSplash = () => {
|
||||
navigator.push(ROUTES.splash, { scroll: false });
|
||||
navigator.push(characterRoutes.splash, { scroll: false });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -46,7 +52,7 @@ export function SplashScreen() {
|
||||
return (
|
||||
<MobileShell background="var(--color-sidebar-background)">
|
||||
<div className={styles.wrapper}>
|
||||
<SplashBackground />
|
||||
<SplashBackground src={character.assets.cover} />
|
||||
{/* 渐变叠加层:accent → transparent (bottom-left → center-right) */}
|
||||
<div className={styles.gradientOverlay} aria-hidden="true" />
|
||||
<div className={styles.content}>
|
||||
@@ -54,6 +60,8 @@ export function SplashScreen() {
|
||||
<div className={styles.spacer} />
|
||||
<SplashLatestMessage
|
||||
message={latestMessage.message}
|
||||
characterName={character.shortName}
|
||||
avatarUrl={character.assets.avatar}
|
||||
isLoading={latestMessage.isLoading}
|
||||
onOpenChat={handleStartChat}
|
||||
/>
|
||||
@@ -62,7 +70,7 @@ export function SplashScreen() {
|
||||
<SplashButton onStartChat={handleStartChat} />
|
||||
</div>
|
||||
<p className={styles.bottom}>
|
||||
Elio Silvestri, Your exclusive AI boyfriend
|
||||
{character.displayName}, {character.copy.splashRelationship}
|
||||
<br />
|
||||
24/7 online | Chat | Companion | Heal | Sweet moments
|
||||
</p>
|
||||
@@ -70,6 +78,7 @@ export function SplashScreen() {
|
||||
<AppBottomNav
|
||||
activeItem="chat"
|
||||
variant="dark"
|
||||
privateRoomLabel={character.copy.privateRoomTitle}
|
||||
onChatClick={handleOpenSplash}
|
||||
onPrivateRoomClick={handleOpenPrivateRoom}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user