Files
Genarrative/src/components/visual-novel-runtime/VisualNovelSavePanel.tsx
2026-05-08 11:44:42 +08:00

103 lines
3.9 KiB
TypeScript

import { Bookmark, Loader2, Play } from 'lucide-react';
import type { ProfileSaveArchiveSummary } from '../../../packages/shared/src/contracts/runtime';
import type { VisualNovelRunSnapshot } from '../../../packages/shared/src/contracts/visualNovel';
type VisualNovelSavePanelProps = {
run: VisualNovelRunSnapshot;
saveArchives?: ProfileSaveArchiveSummary[];
isSaving?: boolean;
isLoadingArchives?: boolean;
resumingWorldKey?: string | null;
onSaveRun?: () => void;
onResumeSaveArchive?: (worldKey: string) => void;
};
function formatArchiveTime(value: string) {
const timestamp = new Date(value).getTime();
if (Number.isNaN(timestamp)) {
return value;
}
return new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(timestamp);
}
export function VisualNovelSavePanel({
run,
saveArchives = [],
isSaving = false,
isLoadingArchives = false,
resumingWorldKey = null,
onSaveRun,
onResumeSaveArchive,
}: VisualNovelSavePanelProps) {
return (
<div className="space-y-3">
<button
type="button"
disabled={!onSaveRun || isSaving}
onClick={onSaveRun}
className="flex min-h-12 w-full items-center justify-center gap-2 rounded-full border border-[var(--platform-subpanel-border)] bg-[var(--platform-button-primary-fill)] px-4 text-sm font-black text-white transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-55"
>
{isSaving ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Bookmark className="h-4 w-4" />
)}
<span>{isSaving ? '保存中' : '保存'}</span>
</button>
{isLoadingArchives ? (
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/74 px-4 py-5 text-center text-sm font-semibold text-[var(--platform-text-soft)]">
</div>
) : saveArchives.length > 0 ? (
<div className="grid gap-3">
{saveArchives.map((entry) => {
const isResuming = resumingWorldKey === entry.worldKey;
return (
<button
key={entry.worldKey}
type="button"
disabled={isResuming || !onResumeSaveArchive}
onClick={() => onResumeSaveArchive?.(entry.worldKey)}
className="flex min-h-20 items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/78 p-3 text-left transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-55"
>
<span className="grid h-11 w-11 shrink-0 place-items-center rounded-[0.85rem] bg-[var(--platform-neutral-bg)] text-[var(--platform-neutral-text)]">
{isResuming ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-black text-[var(--platform-text-strong)]">
{entry.worldName || run.profileId}
</span>
<span className="mt-1 block truncate text-xs font-semibold text-[var(--platform-text-soft)]">
{entry.subtitle || entry.summaryText || run.runId}
</span>
<span className="mt-1 block text-[11px] font-bold text-[var(--platform-text-soft)]">
{formatArchiveTime(entry.lastPlayedAt)}
</span>
</span>
</button>
);
})}
</div>
) : (
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/74 px-4 py-5 text-center text-sm font-semibold text-[var(--platform-text-soft)]">
</div>
)}
</div>
);
}
export default VisualNovelSavePanel;