feat: add platform browse history tab updates
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { type ComponentType, useMemo } from 'react';
|
||||
import {
|
||||
BookOpen,
|
||||
Camera,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
Ticket,
|
||||
UserPlus,
|
||||
} from 'lucide-react';
|
||||
import { type ComponentType, useMemo } from 'react';
|
||||
|
||||
import type {
|
||||
CustomWorldGalleryCard,
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import type { PlatformBrowseHistoryEntry } from '../../services/platformBrowseHistory';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import { getNineSliceStyle, UI_CHROME } from '../../uiAssets';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
resolvePlatformWorldLeadPortrait,
|
||||
} from './platformWorldPresentation';
|
||||
|
||||
export type PlatformHomeTab = 'home' | 'create' | 'profile';
|
||||
export type PlatformHomeTab = 'home' | 'create' | 'discover' | 'profile';
|
||||
|
||||
function SectionHeader({ title, detail }: { title: string; detail: string }) {
|
||||
return (
|
||||
@@ -312,6 +313,7 @@ export function PlatformHomeView({
|
||||
featuredEntries,
|
||||
latestEntries,
|
||||
myEntries,
|
||||
historyEntries,
|
||||
isLoadingPlatform,
|
||||
platformError,
|
||||
onContinueGame,
|
||||
@@ -327,6 +329,7 @@ export function PlatformHomeView({
|
||||
featuredEntries: CustomWorldGalleryCard[];
|
||||
latestEntries: CustomWorldGalleryCard[];
|
||||
myEntries: CustomWorldLibraryEntry<CustomWorldProfile>[];
|
||||
historyEntries: PlatformBrowseHistoryEntry[];
|
||||
isLoadingPlatform: boolean;
|
||||
platformError: string | null;
|
||||
onContinueGame: () => void;
|
||||
@@ -365,6 +368,8 @@ export function PlatformHomeView({
|
||||
const tabIcons = {
|
||||
home: "/Icons/Admurin's Pixel Items/Admurin's Pixel Items/Miscellaneous/Singles/192_RustyTrinket_House.png",
|
||||
create: '/Icons/01_Scroll.png',
|
||||
discover:
|
||||
"/Icons/Admurin's Pixel Items/Admurin's Pixel Items/Miscellaneous/Singles/321_Compass.png",
|
||||
profile: '/UI/Icon_Eq_Head.png',
|
||||
} as const;
|
||||
const recentPlayItems = savedSnapshot
|
||||
@@ -519,6 +524,49 @@ export function PlatformHomeView({
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === 'discover') {
|
||||
content = (
|
||||
<div className="space-y-4 pb-2">
|
||||
<section
|
||||
className="pixel-nine-slice"
|
||||
style={getNineSliceStyle(UI_CHROME.panel, {
|
||||
paddingX: 18,
|
||||
paddingY: 16,
|
||||
})}
|
||||
>
|
||||
<div className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[10px] tracking-[0.2em] text-zinc-100">
|
||||
DISCOVER
|
||||
</div>
|
||||
<div className="mt-4 text-3xl font-black text-white">发现频道</div>
|
||||
<div className="mt-2 max-w-[28rem] text-sm leading-6 text-zinc-300">
|
||||
这里会放后续的专题策展、内容聚合和更多平台频道。首版先保留一个干净的发现位,方便后续扩展。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<SectionHeader title="最近上新" detail="先看广场里的新内容" />
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取推荐内容..." />
|
||||
) : latestEntries.length > 0 ? (
|
||||
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{latestEntries.map((entry: CustomWorldGalleryCard) => (
|
||||
<WorldCard
|
||||
key={`${entry.ownerUserId}:${entry.profileId}:discover`}
|
||||
entry={entry}
|
||||
badge={formatPlatformWorldTime(entry.publishedAt)}
|
||||
metaLabel={describePlatformThemeLabel(entry.themeMode)}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="发现频道暂时还没有可展示的内容。" />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === 'profile') {
|
||||
content = (
|
||||
<div className="space-y-4 pb-2">
|
||||
@@ -662,6 +710,77 @@ export function PlatformHomeView({
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="pixel-nine-slice"
|
||||
style={getNineSliceStyle(UI_CHROME.panel, {
|
||||
paddingX: 16,
|
||||
paddingY: 14,
|
||||
})}
|
||||
>
|
||||
<SectionHeader title="历史浏览" detail="最近看过的作品" />
|
||||
{historyEntries.length > 0 ? (
|
||||
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{historyEntries.map((entry) => (
|
||||
<button
|
||||
key={`${entry.ownerUserId}:${entry.profileId}:history`}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onOpenGalleryDetail({
|
||||
ownerUserId: entry.ownerUserId,
|
||||
profileId: entry.profileId,
|
||||
visibility: 'published',
|
||||
publishedAt: entry.visitedAt,
|
||||
updatedAt: entry.visitedAt,
|
||||
worldName: entry.worldName,
|
||||
subtitle: entry.subtitle,
|
||||
summaryText: entry.summaryText,
|
||||
coverImageSrc: entry.coverImageSrc,
|
||||
themeMode: entry.themeMode,
|
||||
authorDisplayName: entry.authorDisplayName,
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
})
|
||||
}
|
||||
className="relative flex h-[10.5rem] w-[17rem] shrink-0 overflow-hidden rounded-[1.4rem] border border-white/10 bg-[linear-gradient(135deg,rgba(25,32,46,0.95),rgba(9,12,18,0.96))] p-4 text-left"
|
||||
>
|
||||
{entry.coverImageSrc ? (
|
||||
<img
|
||||
src={entry.coverImageSrc}
|
||||
alt={entry.worldName}
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-22"
|
||||
style={{ imageRendering: 'pixelated' }}
|
||||
/>
|
||||
) : null}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(8,10,14,0.08),rgba(8,10,14,0.9))]" />
|
||||
<div className="relative z-10 flex h-full flex-col">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span className="rounded-full border border-amber-300/20 bg-amber-500/10 px-3 py-1 text-[10px] tracking-[0.2em] text-amber-100">
|
||||
HISTORY
|
||||
</span>
|
||||
<span className="text-[11px] text-zinc-400">
|
||||
{formatSnapshotTime(entry.visitedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<div className="line-clamp-1 text-xl font-black text-white">
|
||||
{entry.worldName}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-zinc-300">
|
||||
作者:{entry.authorDisplayName}
|
||||
</div>
|
||||
<div className="mt-2 line-clamp-3 text-xs leading-5 text-zinc-400">
|
||||
{entry.summaryText || entry.subtitle || '等待补充世界摘要。'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="你最近还没有浏览过作品详情,去首页或发现逛一逛吧。" />
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="pixel-nine-slice"
|
||||
style={getNineSliceStyle(UI_CHROME.panel, {
|
||||
@@ -726,7 +845,7 @@ export function PlatformHomeView({
|
||||
className="mt-4 border-t border-white/5 pt-3"
|
||||
style={{ paddingBottom: 'calc(env(safe-area-inset-bottom) + 0.2rem)' }}
|
||||
>
|
||||
<div className="grid h-14 grid-cols-3 gap-1 rounded-[1.2rem] bg-black/18 px-1 py-1">
|
||||
<div className="grid h-14 grid-cols-4 gap-1 rounded-[1.2rem] bg-black/18 px-1 py-1">
|
||||
<PlatformTabButton
|
||||
active={activeTab === 'home'}
|
||||
label="首页"
|
||||
@@ -739,6 +858,12 @@ export function PlatformHomeView({
|
||||
iconSrc={tabIcons.create}
|
||||
onClick={() => onTabChange('create')}
|
||||
/>
|
||||
<PlatformTabButton
|
||||
active={activeTab === 'discover'}
|
||||
label="发现"
|
||||
iconSrc={tabIcons.discover}
|
||||
onClick={() => onTabChange('discover')}
|
||||
/>
|
||||
<PlatformTabButton
|
||||
active={activeTab === 'profile'}
|
||||
label="我的"
|
||||
|
||||
@@ -35,17 +35,16 @@ import {
|
||||
readCustomWorldAgentUiState,
|
||||
writeCustomWorldAgentUiState,
|
||||
} from '../../services/customWorldAgentUiState';
|
||||
import {
|
||||
buildAgentDraftFoundationGenerationProgress,
|
||||
buildAgentDraftFoundationSettingText,
|
||||
isDraftFoundationOperation,
|
||||
isDraftFoundationOperationRunning,
|
||||
} from '../../services/customWorldAgentGenerationProgress';
|
||||
import {
|
||||
buildCustomWorldCreatorIntentDisplayText,
|
||||
buildCustomWorldCreatorIntentGenerationText,
|
||||
createEmptyCustomWorldCreatorIntent,
|
||||
} from '../../services/customWorldCreatorIntent';
|
||||
import {
|
||||
type PlatformBrowseHistoryEntry,
|
||||
readPlatformBrowseHistory,
|
||||
writePlatformBrowseHistory,
|
||||
} from '../../services/platformBrowseHistory';
|
||||
import {
|
||||
getCustomWorldGalleryDetail,
|
||||
listCustomWorldGallery,
|
||||
@@ -60,6 +59,7 @@ import {
|
||||
type CustomWorldProfile,
|
||||
type GameState,
|
||||
} from '../../types';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PlatformCreationTypeModal } from './PlatformCreationTypeModal';
|
||||
import { type PlatformHomeTab,PlatformHomeView } from './PlatformHomeView';
|
||||
import { PlatformWorldDetailView } from './PlatformWorldDetailView';
|
||||
@@ -101,11 +101,6 @@ export type SelectionStage =
|
||||
| 'custom-world-generating'
|
||||
| 'custom-world-result';
|
||||
|
||||
type CustomWorldGenerationViewSource =
|
||||
| 'classic'
|
||||
| 'agent-draft-foundation'
|
||||
| null;
|
||||
|
||||
type PreGameSelectionFlowProps = {
|
||||
selectionStage: SelectionStage;
|
||||
setSelectionStage: (stage: SelectionStage) => void;
|
||||
@@ -225,6 +220,7 @@ export function PreGameSelectionFlow({
|
||||
handleStartNewGame,
|
||||
handleCustomWorldSelect,
|
||||
}: PreGameSelectionFlowProps) {
|
||||
const authUi = useAuthUi();
|
||||
const initialAgentUiStateRef = useRef(readCustomWorldAgentUiState());
|
||||
const hasAppliedInitialAgentWorkspaceRef = useRef(false);
|
||||
const [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] =
|
||||
@@ -235,6 +231,9 @@ export function PreGameSelectionFlow({
|
||||
const [publishedGalleryEntries, setPublishedGalleryEntries] = useState<
|
||||
CustomWorldGalleryCard[]
|
||||
>([]);
|
||||
const [historyEntries, setHistoryEntries] = useState<
|
||||
PlatformBrowseHistoryEntry[]
|
||||
>([]);
|
||||
const [platformTab, setPlatformTab] = useState<PlatformHomeTab>('home');
|
||||
const [selectedDetailEntry, setSelectedDetailEntry] =
|
||||
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
|
||||
@@ -270,10 +269,6 @@ export function PreGameSelectionFlow({
|
||||
const [isMutatingDetail, setIsMutatingDetail] = useState(false);
|
||||
const [customWorldProgress, setCustomWorldProgress] =
|
||||
useState<CustomWorldGenerationProgress | null>(null);
|
||||
const [customWorldGenerationViewSource, setCustomWorldGenerationViewSource] =
|
||||
useState<CustomWorldGenerationViewSource>(null);
|
||||
const [agentDraftGenerationStartedAt, setAgentDraftGenerationStartedAt] =
|
||||
useState<number | null>(null);
|
||||
const customWorldAbortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const previewCustomWorldCharacters = useMemo(
|
||||
@@ -335,6 +330,23 @@ export function PreGameSelectionFlow({
|
||||
}
|
||||
}, [selectedDetailEntry]);
|
||||
|
||||
const appendBrowseHistoryEntry = useCallback(
|
||||
(entry: {
|
||||
ownerUserId: string;
|
||||
profileId: string;
|
||||
worldName: string;
|
||||
subtitle: string;
|
||||
summaryText: string;
|
||||
coverImageSrc: string | null;
|
||||
themeMode: CustomWorldGalleryCard['themeMode'];
|
||||
authorDisplayName: string;
|
||||
}) => {
|
||||
const nextEntries = writePlatformBrowseHistory(authUi?.user, entry);
|
||||
setHistoryEntries(nextEntries);
|
||||
},
|
||||
[authUi?.user],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAppliedInitialAgentWorkspaceRef.current) {
|
||||
return;
|
||||
@@ -363,6 +375,7 @@ export function PreGameSelectionFlow({
|
||||
}
|
||||
setSavedCustomWorldEntries(libraryEntries);
|
||||
setPublishedGalleryEntries(galleryEntries);
|
||||
setHistoryEntries(readPlatformBrowseHistory(authUi?.user));
|
||||
} catch (error) {
|
||||
if (!isActive) {
|
||||
return;
|
||||
@@ -378,7 +391,7 @@ export function PreGameSelectionFlow({
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, []);
|
||||
}, [authUi?.user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -527,40 +540,10 @@ export function PreGameSelectionFlow({
|
||||
return customWorldCreatorIntent.rawSettingText.trim();
|
||||
}, [customWorldCreatorIntent]);
|
||||
|
||||
const agentDraftSettingPreview = useMemo(
|
||||
() => buildAgentDraftFoundationSettingText(agentSession),
|
||||
[agentSession],
|
||||
);
|
||||
|
||||
const agentDraftGenerationProgress = useMemo(
|
||||
() =>
|
||||
buildAgentDraftFoundationGenerationProgress(
|
||||
agentOperation,
|
||||
agentDraftGenerationStartedAt,
|
||||
),
|
||||
[agentDraftGenerationStartedAt, agentOperation],
|
||||
);
|
||||
|
||||
const isAgentDraftGenerationView =
|
||||
customWorldGenerationViewSource === 'agent-draft-foundation';
|
||||
const activeGenerationProgress = isAgentDraftGenerationView
|
||||
? agentDraftGenerationProgress
|
||||
: customWorldProgress;
|
||||
const isActiveGenerationRunning = isAgentDraftGenerationView
|
||||
? isDraftFoundationOperationRunning(agentOperation)
|
||||
: isGeneratingCustomWorld;
|
||||
const activeGenerationError =
|
||||
isAgentDraftGenerationView &&
|
||||
isDraftFoundationOperation(agentOperation) &&
|
||||
agentOperation.status === 'failed'
|
||||
? agentOperation.error || agentOperation.phaseDetail
|
||||
: customWorldError;
|
||||
|
||||
const leaveCustomWorldResult = () => {
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
|
||||
};
|
||||
|
||||
@@ -571,7 +554,6 @@ export function PreGameSelectionFlow({
|
||||
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setSelectionStage('platform');
|
||||
};
|
||||
|
||||
@@ -717,6 +699,16 @@ export function PreGameSelectionFlow({
|
||||
const openLibraryDetail = (
|
||||
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
|
||||
) => {
|
||||
appendBrowseHistoryEntry({
|
||||
ownerUserId: entry.ownerUserId,
|
||||
profileId: entry.profileId,
|
||||
worldName: entry.worldName,
|
||||
subtitle: entry.subtitle,
|
||||
summaryText: entry.summaryText,
|
||||
coverImageSrc: entry.coverImageSrc,
|
||||
themeMode: entry.themeMode,
|
||||
authorDisplayName: entry.authorDisplayName,
|
||||
});
|
||||
setSelectedDetailEntry(entry);
|
||||
setDetailError(null);
|
||||
setSelectionStage('detail');
|
||||
@@ -731,6 +723,16 @@ export function PreGameSelectionFlow({
|
||||
entry.ownerUserId,
|
||||
entry.profileId,
|
||||
);
|
||||
appendBrowseHistoryEntry({
|
||||
ownerUserId: detailEntry.ownerUserId,
|
||||
profileId: detailEntry.profileId,
|
||||
worldName: detailEntry.worldName,
|
||||
subtitle: detailEntry.subtitle,
|
||||
summaryText: detailEntry.summaryText,
|
||||
coverImageSrc: detailEntry.coverImageSrc,
|
||||
themeMode: detailEntry.themeMode,
|
||||
authorDisplayName: detailEntry.authorDisplayName,
|
||||
});
|
||||
setSelectedDetailEntry(detailEntry);
|
||||
} catch (error) {
|
||||
setSelectedDetailEntry(null);
|
||||
@@ -1039,6 +1041,7 @@ export function PreGameSelectionFlow({
|
||||
featuredEntries={featuredGalleryEntries}
|
||||
latestEntries={publishedGalleryEntries}
|
||||
myEntries={savedCustomWorldEntries}
|
||||
historyEntries={historyEntries}
|
||||
isLoadingPlatform={isLoadingPlatform}
|
||||
platformError={
|
||||
isLoadingPlatform ? null : (platformError ?? creationTypeError)
|
||||
|
||||
133
src/services/platformBrowseHistory.ts
Normal file
133
src/services/platformBrowseHistory.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { CustomWorldGalleryCard } from '../../packages/shared/src/contracts/runtime';
|
||||
import type { AuthUser } from './authService';
|
||||
|
||||
export type PlatformBrowseHistoryEntry = {
|
||||
ownerUserId: string;
|
||||
profileId: string;
|
||||
worldName: string;
|
||||
subtitle: string;
|
||||
summaryText: string;
|
||||
coverImageSrc: string | null;
|
||||
themeMode: CustomWorldGalleryCard['themeMode'];
|
||||
authorDisplayName: string;
|
||||
visitedAt: string;
|
||||
};
|
||||
|
||||
const HISTORY_STORAGE_KEY_PREFIX = 'genarrative.platform.browse-history.v1';
|
||||
const MAX_HISTORY_ENTRIES = 20;
|
||||
|
||||
function canUseLocalStorage() {
|
||||
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
|
||||
}
|
||||
|
||||
function buildHistoryStorageKey(user: AuthUser | null | undefined) {
|
||||
const accountId = user?.id?.trim() || user?.username?.trim() || 'guest';
|
||||
return `${HISTORY_STORAGE_KEY_PREFIX}:${accountId}`;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readString(value: unknown) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function normalizeHistoryEntry(value: unknown): PlatformBrowseHistoryEntry | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ownerUserId = readString(value.ownerUserId);
|
||||
const profileId = readString(value.profileId);
|
||||
const worldName = readString(value.worldName);
|
||||
const visitedAt = readString(value.visitedAt);
|
||||
if (!ownerUserId || !profileId || !worldName || !visitedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const themeMode = readString(value.themeMode) as PlatformBrowseHistoryEntry['themeMode'];
|
||||
|
||||
return {
|
||||
ownerUserId,
|
||||
profileId,
|
||||
worldName,
|
||||
subtitle: readString(value.subtitle),
|
||||
summaryText: readString(value.summaryText),
|
||||
coverImageSrc: readString(value.coverImageSrc) || null,
|
||||
themeMode: themeMode || 'mythic',
|
||||
authorDisplayName: readString(value.authorDisplayName) || '玩家',
|
||||
visitedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function sortHistoryEntries(entries: PlatformBrowseHistoryEntry[]) {
|
||||
return [...entries].sort((left, right) => {
|
||||
return (
|
||||
new Date(right.visitedAt).getTime() - new Date(left.visitedAt).getTime()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function readPlatformBrowseHistory(user: AuthUser | null | undefined) {
|
||||
if (!canUseLocalStorage()) {
|
||||
return [] as PlatformBrowseHistoryEntry[];
|
||||
}
|
||||
|
||||
const raw = window.localStorage.getItem(buildHistoryStorageKey(user));
|
||||
if (!raw?.trim()) {
|
||||
return [] as PlatformBrowseHistoryEntry[];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown[];
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [] as PlatformBrowseHistoryEntry[];
|
||||
}
|
||||
|
||||
return sortHistoryEntries(
|
||||
parsed
|
||||
.map((entry) => normalizeHistoryEntry(entry))
|
||||
.filter((entry): entry is PlatformBrowseHistoryEntry => Boolean(entry)),
|
||||
).slice(0, MAX_HISTORY_ENTRIES);
|
||||
} catch {
|
||||
return [] as PlatformBrowseHistoryEntry[];
|
||||
}
|
||||
}
|
||||
|
||||
export function writePlatformBrowseHistory(
|
||||
user: AuthUser | null | undefined,
|
||||
entry: Omit<PlatformBrowseHistoryEntry, 'visitedAt'> & {
|
||||
visitedAt?: string;
|
||||
},
|
||||
) {
|
||||
if (!canUseLocalStorage()) {
|
||||
return [] as PlatformBrowseHistoryEntry[];
|
||||
}
|
||||
|
||||
const nextEntry: PlatformBrowseHistoryEntry = {
|
||||
...entry,
|
||||
subtitle: entry.subtitle?.trim() || '',
|
||||
summaryText: entry.summaryText?.trim() || '',
|
||||
coverImageSrc: entry.coverImageSrc?.trim() || null,
|
||||
authorDisplayName: entry.authorDisplayName?.trim() || '玩家',
|
||||
visitedAt: entry.visitedAt?.trim() || new Date().toISOString(),
|
||||
};
|
||||
const deduped = readPlatformBrowseHistory(user).filter(
|
||||
(current) =>
|
||||
!(
|
||||
current.ownerUserId === nextEntry.ownerUserId &&
|
||||
current.profileId === nextEntry.profileId
|
||||
),
|
||||
);
|
||||
const nextEntries = sortHistoryEntries([nextEntry, ...deduped]).slice(
|
||||
0,
|
||||
MAX_HISTORY_ENTRIES,
|
||||
);
|
||||
|
||||
window.localStorage.setItem(
|
||||
buildHistoryStorageKey(user),
|
||||
JSON.stringify(nextEntries),
|
||||
);
|
||||
return nextEntries;
|
||||
}
|
||||
Reference in New Issue
Block a user