preview mode
This commit is contained in:
+4
-124
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { type CSSProperties, useEffect, useRef, forwardRef, useImperativeHandle, useCallback } from "react";
|
||||
import { type CSSProperties, useEffect, useRef, forwardRef, useImperativeHandle } from "react";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
|
||||
@@ -27,6 +27,7 @@ export type MapHandle = {
|
||||
bearing: number;
|
||||
projection: string;
|
||||
} | null;
|
||||
getMap: () => import("maplibre-gl").Map | null;
|
||||
};
|
||||
|
||||
type MapProps = {
|
||||
@@ -51,10 +52,6 @@ type MapProps = {
|
||||
focusFeatureCollection?: FeatureCollection | null;
|
||||
focusRequestKey?: string | number | null;
|
||||
focusPadding?: number | import("maplibre-gl").PaddingOptions;
|
||||
hideOutside?: boolean;
|
||||
onToggleHideOutside?: () => void;
|
||||
onUndoReplay?: () => void;
|
||||
canUndoReplay?: boolean;
|
||||
};
|
||||
|
||||
const Map = forwardRef<MapHandle, MapProps>(function Map({
|
||||
@@ -79,10 +76,6 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
||||
focusFeatureCollection = null,
|
||||
focusRequestKey = null,
|
||||
focusPadding,
|
||||
hideOutside = false,
|
||||
onToggleHideOutside,
|
||||
onUndoReplay,
|
||||
canUndoReplay = false,
|
||||
}, ref) {
|
||||
const modeRef = useRef<MapProps["mode"]>(mode);
|
||||
const draftRef = useRef<FeatureCollection>(draft);
|
||||
@@ -119,15 +112,8 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getViewState,
|
||||
}), [getViewState]);
|
||||
|
||||
const handleLogViewState = useCallback(() => {
|
||||
const state = getViewState();
|
||||
console.log("Current Map View State:", state);
|
||||
if (state) {
|
||||
alert(`Captured View State:\nCenter: ${state.center.lng.toFixed(4)}, ${state.center.lat.toFixed(4)}\nZoom: ${state.zoom.toFixed(2)}\nPitch: ${state.pitch.toFixed(1)}°\nBearing: ${state.bearing.toFixed(1)}°\nProjection: ${state.projection}`);
|
||||
}
|
||||
}, [getViewState]);
|
||||
getMap: () => mapRef.current,
|
||||
}), [getViewState, mapRef]);
|
||||
|
||||
const {
|
||||
editingEngineRef,
|
||||
@@ -256,112 +242,6 @@ const Map = forwardRef<MapHandle, MapProps>(function Map({
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{mode === "replay" && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSetMode?.("select")}
|
||||
style={{
|
||||
...zoomButtonStyle,
|
||||
width: "auto",
|
||||
padding: "0 12px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 700,
|
||||
background: "#7f1d1d",
|
||||
color: "white",
|
||||
border: "1px solid #991b1b",
|
||||
borderRadius: "999px",
|
||||
cursor: "pointer",
|
||||
marginRight: "4px",
|
||||
}}
|
||||
>
|
||||
Thoát Replay Edit
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogViewState}
|
||||
title="Capture current map view state"
|
||||
style={{
|
||||
...zoomButtonStyle,
|
||||
width: "auto",
|
||||
padding: "0 12px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 700,
|
||||
background: "#1e293b",
|
||||
color: "#38bdf8",
|
||||
border: "1px solid #334155",
|
||||
borderRadius: "999px",
|
||||
cursor: "pointer",
|
||||
marginRight: "8px",
|
||||
}}
|
||||
>
|
||||
Capture View
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onUndoReplay}
|
||||
disabled={!onUndoReplay || !canUndoReplay}
|
||||
title="Undo thao tác replay gần nhất"
|
||||
style={{
|
||||
...zoomButtonStyle,
|
||||
width: "auto",
|
||||
padding: "0 12px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 700,
|
||||
background: !onUndoReplay || !canUndoReplay ? "#0f172a" : "#1e293b",
|
||||
color: !onUndoReplay || !canUndoReplay ? "#64748b" : "#f8fafc",
|
||||
border: "1px solid #334155",
|
||||
borderRadius: "999px",
|
||||
cursor: !onUndoReplay || !canUndoReplay ? "not-allowed" : "pointer",
|
||||
marginRight: "8px",
|
||||
}}
|
||||
>
|
||||
Undo Replay
|
||||
</button>
|
||||
|
||||
<div
|
||||
onClick={onToggleHideOutside}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
cursor: "pointer",
|
||||
marginRight: "8px",
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "32px",
|
||||
height: "18px",
|
||||
borderRadius: "10px",
|
||||
background: hideOutside ? "#e11d48" : "#334155",
|
||||
position: "relative",
|
||||
transition: "background 0.2s",
|
||||
border: "1px solid rgba(255,255,255,0.1)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "2px",
|
||||
left: hideOutside ? "16px" : "2px",
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
background: "white",
|
||||
transition: "left 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
boxShadow: "0 1px 2px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: "1px", height: "20px", background: "rgba(148, 163, 184, 0.3)", marginRight: "4px" }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<label
|
||||
title={
|
||||
isGlobeProjection
|
||||
|
||||
@@ -43,5 +43,12 @@ export function ModeHint({ mode }: { mode: EditorMode }) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (mode === "replay_preview") {
|
||||
return (
|
||||
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
||||
Đang xem preview replay trên session tách biệt.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ const uiOptionChoices: Array<{ label: string; value: UIOptionName }> = [
|
||||
{ label: "Timeline", value: "timeline" },
|
||||
{ label: "Layer Panel", value: "layer_panel" },
|
||||
{ label: "Wiki Panel", value: "wiki_panel" },
|
||||
{ label: "Close Wiki Panel", value: "close_wiki_panel" },
|
||||
{ label: "Zoom Panel", value: "zoom_panel" },
|
||||
{ label: "Wiki", value: "wiki" },
|
||||
{ label: "Toast", value: "toast" },
|
||||
@@ -96,6 +97,7 @@ const uiSimpleOptionValues: UIOptionName[] = [
|
||||
"timeline",
|
||||
"layer_panel",
|
||||
"wiki_panel",
|
||||
"close_wiki_panel",
|
||||
"zoom_panel",
|
||||
];
|
||||
|
||||
@@ -153,6 +155,13 @@ const narrativeActionDefinitions: NarrativeActionDefinitionMap = {
|
||||
deserialize: (params) => ({ title: asString(params[0]) }),
|
||||
serialize: (values) => [asString(values.title)],
|
||||
},
|
||||
clear_title: {
|
||||
label: "Xóa tiêu đề",
|
||||
fields: [],
|
||||
create: () => ({ function_name: "clear_title", params: [] }),
|
||||
deserialize: () => ({}),
|
||||
serialize: () => [],
|
||||
},
|
||||
set_descriptions: {
|
||||
label: "Mô tả",
|
||||
fields: [{ name: "text", label: "Text", kind: "textarea", placeholder: "Nội dung diễn giải" }],
|
||||
@@ -160,6 +169,13 @@ const narrativeActionDefinitions: NarrativeActionDefinitionMap = {
|
||||
deserialize: (params) => ({ text: asString(params[0]) }),
|
||||
serialize: (values) => [asString(values.text)],
|
||||
},
|
||||
clear_descriptions: {
|
||||
label: "Xóa mô tả",
|
||||
fields: [],
|
||||
create: () => ({ function_name: "clear_descriptions", params: [] }),
|
||||
deserialize: () => ({}),
|
||||
serialize: () => [],
|
||||
},
|
||||
show_dialog_box: {
|
||||
label: "Dialog box",
|
||||
fields: [
|
||||
@@ -190,6 +206,13 @@ const narrativeActionDefinitions: NarrativeActionDefinitionMap = {
|
||||
asString(values.speaker),
|
||||
],
|
||||
},
|
||||
clear_dialog_box: {
|
||||
label: "Đóng dialog box",
|
||||
fields: [],
|
||||
create: () => ({ function_name: "clear_dialog_box", params: [] }),
|
||||
deserialize: () => ({}),
|
||||
serialize: () => [],
|
||||
},
|
||||
display_historical_image: {
|
||||
label: "Ảnh lịch sử",
|
||||
fields: [
|
||||
@@ -206,6 +229,13 @@ const narrativeActionDefinitions: NarrativeActionDefinitionMap = {
|
||||
emptyToUndefined(asString(values.caption)),
|
||||
]),
|
||||
},
|
||||
clear_historical_image: {
|
||||
label: "Xóa ảnh lịch sử",
|
||||
fields: [],
|
||||
create: () => ({ function_name: "clear_historical_image", params: [] }),
|
||||
deserialize: () => ({}),
|
||||
serialize: () => [],
|
||||
},
|
||||
set_step_subtitle: {
|
||||
label: "Phụ đề",
|
||||
fields: [{ name: "subtitle", label: "Subtitle", kind: "textarea", placeholder: "Để trống để ẩn subtitle" }],
|
||||
@@ -213,6 +243,13 @@ const narrativeActionDefinitions: NarrativeActionDefinitionMap = {
|
||||
deserialize: (params) => ({ subtitle: params[0] == null ? "" : asString(params[0]) }),
|
||||
serialize: (values) => [emptyToNull(asString(values.subtitle))],
|
||||
},
|
||||
clear_step_subtitle: {
|
||||
label: "Xóa phụ đề",
|
||||
fields: [],
|
||||
create: () => ({ function_name: "clear_step_subtitle", params: [] }),
|
||||
deserialize: () => ({}),
|
||||
serialize: () => [],
|
||||
},
|
||||
};
|
||||
|
||||
export default function ReplayEffectsSidebar({
|
||||
@@ -451,6 +488,16 @@ function MapFunctionShortcutPanel({
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ShortcutButton
|
||||
label="Show All Geo"
|
||||
tone="green"
|
||||
onClick={() =>
|
||||
onAppendActions(
|
||||
[{ function_name: "show_all_geometries", params: [] }],
|
||||
"Map: show all geometries"
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
@@ -576,13 +623,13 @@ function GeoFunctionShortcutPanel({
|
||||
}
|
||||
/>
|
||||
<ShortcutButton
|
||||
label="Dim Others"
|
||||
label="Hide Others"
|
||||
tone="slate"
|
||||
disabled={!hasSelection}
|
||||
onClick={() =>
|
||||
onAppendActions(
|
||||
[{ function_name: "dim_other_geometries", params: [selectedIds, 0.18] }],
|
||||
`Geo: dim others ngoài ${selectedCount} geo`
|
||||
[{ function_name: "dim_other_geometries", params: [selectedIds] }],
|
||||
`Geo: hide others ngoài ${selectedCount} geo`
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -1402,6 +1449,7 @@ function buildEmptyUiOptionSelection(): Record<UIOptionName, boolean> {
|
||||
timeline: false,
|
||||
layer_panel: false,
|
||||
wiki_panel: false,
|
||||
close_wiki_panel: false,
|
||||
zoom_panel: false,
|
||||
wiki: false,
|
||||
toast: false,
|
||||
@@ -1523,6 +1571,11 @@ function buildUiOptionAction(
|
||||
function_name: option,
|
||||
params: [false],
|
||||
};
|
||||
case "close_wiki_panel":
|
||||
return {
|
||||
function_name: option,
|
||||
params: [],
|
||||
};
|
||||
case "wiki":
|
||||
return {
|
||||
function_name: option,
|
||||
@@ -1574,6 +1627,7 @@ function normalizeUiOptionValue(value: unknown): UIOptionName | null {
|
||||
case "timeline":
|
||||
case "layer_panel":
|
||||
case "wiki_panel":
|
||||
case "close_wiki_panel":
|
||||
case "zoom_panel":
|
||||
case "wiki":
|
||||
case "toast":
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import type {
|
||||
ReplayPreviewDialog,
|
||||
ReplayPreviewImage,
|
||||
ReplayPreviewToast,
|
||||
} from "@/uhm/lib/replay/useReplayPreview";
|
||||
|
||||
type Props = {
|
||||
isPreviewMode: boolean;
|
||||
isPlaying: boolean;
|
||||
title: string;
|
||||
descriptions: string;
|
||||
subtitle: string | null;
|
||||
dialog: ReplayPreviewDialog | null;
|
||||
image: ReplayPreviewImage | null;
|
||||
toasts: ReplayPreviewToast[];
|
||||
sidebarOpen: boolean;
|
||||
playbackSpeed: number;
|
||||
activeStepLabel: string | null;
|
||||
activeStepNumber: number | null;
|
||||
totalSteps: number;
|
||||
onPlayPreview: () => void;
|
||||
onStopPreview: () => void;
|
||||
onResetPreview: () => void;
|
||||
onExitPreview: () => void;
|
||||
};
|
||||
|
||||
export default function ReplayPreviewOverlay({
|
||||
isPreviewMode,
|
||||
isPlaying,
|
||||
title,
|
||||
descriptions,
|
||||
subtitle,
|
||||
dialog,
|
||||
image,
|
||||
toasts,
|
||||
sidebarOpen,
|
||||
playbackSpeed,
|
||||
activeStepLabel,
|
||||
activeStepNumber,
|
||||
totalSteps,
|
||||
onPlayPreview,
|
||||
onStopPreview,
|
||||
onResetPreview,
|
||||
onExitPreview,
|
||||
}: Props) {
|
||||
const hasNarrativeCard = title.trim().length > 0 || descriptions.trim().length > 0;
|
||||
const hasWikiPreview = sidebarOpen;
|
||||
const shouldRender =
|
||||
isPreviewMode ||
|
||||
isPlaying ||
|
||||
hasNarrativeCard ||
|
||||
Boolean(subtitle) ||
|
||||
Boolean(dialog) ||
|
||||
Boolean(image) ||
|
||||
Boolean(toasts.length);
|
||||
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
zIndex: 15,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{hasNarrativeCard ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 72,
|
||||
left: 18,
|
||||
maxWidth: 460,
|
||||
borderRadius: 18,
|
||||
border: "1px solid rgba(148, 163, 184, 0.26)",
|
||||
background: "linear-gradient(145deg, rgba(15, 23, 42, 0.94), rgba(30, 41, 59, 0.88))",
|
||||
boxShadow: "0 14px 42px rgba(2, 6, 23, 0.42)",
|
||||
padding: "18px 20px",
|
||||
}}
|
||||
>
|
||||
{title.trim().length ? (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 26,
|
||||
lineHeight: 1.1,
|
||||
fontWeight: 900,
|
||||
color: "#f8fafc",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
{descriptions.trim().length ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: title.trim().length ? 12 : 0,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.55,
|
||||
color: "#dbeafe",
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{descriptions}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{toasts.length ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 72,
|
||||
right: hasWikiPreview ? 454 : 18,
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
width: 280,
|
||||
}}
|
||||
>
|
||||
{toasts.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
border: "1px solid rgba(56, 189, 248, 0.28)",
|
||||
background: "rgba(8, 47, 73, 0.9)",
|
||||
color: "#e0f2fe",
|
||||
padding: "12px 14px",
|
||||
fontSize: 13,
|
||||
lineHeight: 1.4,
|
||||
boxShadow: "0 10px 26px rgba(2, 6, 23, 0.32)",
|
||||
}}
|
||||
>
|
||||
{toast.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{image ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 18,
|
||||
bottom: 96,
|
||||
width: 320,
|
||||
borderRadius: 18,
|
||||
overflow: "hidden",
|
||||
border: "1px solid rgba(148, 163, 184, 0.22)",
|
||||
background: "rgba(15, 23, 42, 0.9)",
|
||||
boxShadow: "0 16px 44px rgba(2, 6, 23, 0.42)",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.caption || "Historical image"}
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "block",
|
||||
maxHeight: 240,
|
||||
objectFit: "cover",
|
||||
background: "#020617",
|
||||
}}
|
||||
/>
|
||||
{image.caption?.trim() ? (
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 12px",
|
||||
fontSize: 12,
|
||||
lineHeight: 1.45,
|
||||
color: "#cbd5e1",
|
||||
}}
|
||||
>
|
||||
{image.caption}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{dialog ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: dialog.side === "right" ? "auto" : 18,
|
||||
right: dialog.side === "right" ? 18 : "auto",
|
||||
bottom: subtitle ? 138 : 96,
|
||||
maxWidth: 420,
|
||||
display: "grid",
|
||||
gap: 10,
|
||||
gridTemplateColumns: dialog.avatar.trim().length ? "56px 1fr" : "1fr",
|
||||
alignItems: "start",
|
||||
}}
|
||||
>
|
||||
{dialog.avatar.trim().length ? (
|
||||
<img
|
||||
src={dialog.avatar}
|
||||
alt={dialog.speaker || "speaker"}
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: "50%",
|
||||
objectFit: "cover",
|
||||
border: "2px solid rgba(125, 211, 252, 0.55)",
|
||||
background: "#0f172a",
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 18,
|
||||
border: "1px solid rgba(148, 163, 184, 0.24)",
|
||||
background: "rgba(15, 23, 42, 0.92)",
|
||||
padding: "14px 16px",
|
||||
color: "#f8fafc",
|
||||
boxShadow: "0 14px 36px rgba(2, 6, 23, 0.38)",
|
||||
}}
|
||||
>
|
||||
{dialog.speaker?.trim() ? (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 6,
|
||||
fontSize: 11,
|
||||
color: "#7dd3fc",
|
||||
fontWeight: 900,
|
||||
letterSpacing: 0.4,
|
||||
}}
|
||||
>
|
||||
{dialog.speaker}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.5,
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{dialog.text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{subtitle?.trim() ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
bottom: 90,
|
||||
transform: "translateX(-50%)",
|
||||
maxWidth: 720,
|
||||
borderRadius: 999,
|
||||
border: "1px solid rgba(148, 163, 184, 0.24)",
|
||||
background: "rgba(2, 6, 23, 0.84)",
|
||||
color: "#f8fafc",
|
||||
padding: "10px 18px",
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.45,
|
||||
textAlign: "center",
|
||||
boxShadow: "0 12px 32px rgba(2, 6, 23, 0.28)",
|
||||
}}
|
||||
>
|
||||
{subtitle}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isPreviewMode ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 64,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
width: "min(520px, calc(100% - 72px))",
|
||||
borderRadius: 18,
|
||||
border: "1px solid rgba(148, 163, 184, 0.24)",
|
||||
background: "rgba(15, 23, 42, 0.9)",
|
||||
color: "#e2e8f0",
|
||||
padding: "12px 14px",
|
||||
boxShadow: "0 12px 32px rgba(2, 6, 23, 0.3)",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "grid", gap: 6, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
padding: "3px 8px",
|
||||
borderRadius: 999,
|
||||
background: "rgba(34, 197, 94, 0.2)",
|
||||
color: "#86efac",
|
||||
fontWeight: 900,
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.3,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</span>
|
||||
{activeStepLabel ? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 800,
|
||||
color: "#f8fafc",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
>
|
||||
{activeStepLabel}
|
||||
</span>
|
||||
) : null}
|
||||
<span style={{ fontSize: 12, color: "#94a3b8" }}>
|
||||
x{playbackSpeed.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
{totalSteps > 0 ? (
|
||||
<div style={{ display: "grid", gap: 6 }}>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 6,
|
||||
borderRadius: 999,
|
||||
background: "rgba(51, 65, 85, 0.8)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, ((activeStepNumber || 0) / totalSteps) * 100))}%`,
|
||||
height: "100%",
|
||||
borderRadius: 999,
|
||||
background: "linear-gradient(90deg, #22c55e, #38bdf8)",
|
||||
transition: "width 180ms ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "#94a3b8" }}>
|
||||
Step {activeStepNumber || 0}/{totalSteps}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, flex: "0 0 auto" }}>
|
||||
{isPlaying ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStopPreview}
|
||||
style={previewButtonStyle("#7f1d1d")}
|
||||
>
|
||||
Dừng
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onResetPreview}
|
||||
style={previewButtonStyle("#1e3a8a")}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPlayPreview}
|
||||
style={previewButtonStyle("#166534")}
|
||||
>
|
||||
Phát lại
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onExitPreview}
|
||||
style={previewButtonStyle("#334155")}
|
||||
>
|
||||
Thoát preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function previewButtonStyle(background: string): CSSProperties {
|
||||
return {
|
||||
border: "none",
|
||||
background,
|
||||
color: "white",
|
||||
borderRadius: 10,
|
||||
padding: "8px 12px",
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.08)",
|
||||
};
|
||||
}
|
||||
@@ -27,6 +27,12 @@ type Props = {
|
||||
onMutateReplay: (label: string, mutator: (draftReplay: BattleReplay) => void) => boolean;
|
||||
onUndoReplay: () => void;
|
||||
onExitReplay: () => void;
|
||||
isPreviewPlaying: boolean;
|
||||
previewPlaybackSpeed: number;
|
||||
onPlayPreviewFromStart: () => void;
|
||||
onPlayPreviewFromSelection: () => void;
|
||||
onStopPreview: () => void;
|
||||
onResetPreview: () => void;
|
||||
};
|
||||
|
||||
type ActionGroupKey = "use_UI_function" | "use_map_function" | "use_geo_function" | "use_narrow_function";
|
||||
@@ -72,6 +78,12 @@ export default function ReplayTimelineSidebar({
|
||||
onMutateReplay,
|
||||
onUndoReplay,
|
||||
onExitReplay,
|
||||
isPreviewPlaying,
|
||||
previewPlaybackSpeed,
|
||||
onPlayPreviewFromStart,
|
||||
onPlayPreviewFromSelection,
|
||||
onStopPreview,
|
||||
onResetPreview,
|
||||
}: Props) {
|
||||
const stages = useMemo(() => replay?.detail || [], [replay?.detail]);
|
||||
const selectedStage =
|
||||
@@ -368,6 +380,80 @@ export default function ReplayTimelineSidebar({
|
||||
Thoát replay
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: isPreviewPlaying ? "1fr 1fr" : "1fr 1fr",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPlayPreviewFromStart}
|
||||
disabled={!replay || totalSteps === 0}
|
||||
style={{
|
||||
...buttonStyle,
|
||||
background: !replay || totalSteps === 0 ? "#1e293b" : "#166534",
|
||||
border: "none",
|
||||
cursor: !replay || totalSteps === 0 ? "not-allowed" : "pointer",
|
||||
opacity: !replay || totalSteps === 0 ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
Play từ đầu
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPlayPreviewFromSelection}
|
||||
disabled={!replay || selectedStage == null || selectedStepIndex == null}
|
||||
style={{
|
||||
...buttonStyle,
|
||||
background:
|
||||
!replay || selectedStage == null || selectedStepIndex == null
|
||||
? "#1e293b"
|
||||
: "#0f766e",
|
||||
border: "none",
|
||||
cursor:
|
||||
!replay || selectedStage == null || selectedStepIndex == null
|
||||
? "not-allowed"
|
||||
: "pointer",
|
||||
opacity:
|
||||
!replay || selectedStage == null || selectedStepIndex == null
|
||||
? 0.7
|
||||
: 1,
|
||||
}}
|
||||
>
|
||||
Play từ step
|
||||
</button>
|
||||
{isPreviewPlaying ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStopPreview}
|
||||
style={{
|
||||
...buttonStyle,
|
||||
background: "#7f1d1d",
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
Dừng
|
||||
</button>
|
||||
) : null}
|
||||
{isPreviewPlaying ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onResetPreview}
|
||||
style={{
|
||||
...buttonStyle,
|
||||
background: "#1e3a8a",
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
Reset preview
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "#94a3b8" }}>
|
||||
Preview sẽ mở trong mode riêng với snapshot replay tại thời điểm bấm play.
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
@@ -999,6 +1085,7 @@ const uiOptionLabels: Record<UIOptionName, string> = {
|
||||
timeline: "Timeline",
|
||||
layer_panel: "Layer Panel",
|
||||
wiki_panel: "Wiki Panel",
|
||||
close_wiki_panel: "Đóng Wiki Panel",
|
||||
zoom_panel: "Zoom Panel",
|
||||
wiki: "Wiki",
|
||||
toast: "Toast",
|
||||
@@ -1008,10 +1095,15 @@ const uiOptionLabels: Record<UIOptionName, string> = {
|
||||
|
||||
const narrativeFunctionLabels: Record<NarrativeFunctionName, string> = {
|
||||
set_title: "Tiêu đề step",
|
||||
clear_title: "Xóa tiêu đề",
|
||||
set_descriptions: "Mô tả",
|
||||
clear_descriptions: "Xóa mô tả",
|
||||
show_dialog_box: "Dialog box",
|
||||
clear_dialog_box: "Đóng dialog box",
|
||||
display_historical_image: "Ảnh lịch sử",
|
||||
clear_historical_image: "Xóa ảnh lịch sử",
|
||||
set_step_subtitle: "Phụ đề",
|
||||
clear_step_subtitle: "Xóa phụ đề",
|
||||
};
|
||||
|
||||
const mapFunctionLabels: Record<MapFunctionName, string> = {
|
||||
@@ -1022,6 +1114,7 @@ const mapFunctionLabels: Record<MapFunctionName, string> = {
|
||||
toggle_labels: "Bật/tắt labels",
|
||||
show_labels: "Hiện labels",
|
||||
hide_labels: "Ẩn labels",
|
||||
show_all_geometries: "Hiện tất cả geo",
|
||||
reset_camera_north: "North up",
|
||||
};
|
||||
|
||||
@@ -1039,7 +1132,7 @@ const geoFunctionLabels: Record<GeoFunctionName, string> = {
|
||||
show_geometry_label: "Label geometry",
|
||||
follow_geometry_path: "Follow path",
|
||||
follow_geometries_path: "Follow path",
|
||||
dim_other_geometries: "Làm mờ geo khác",
|
||||
dim_other_geometries: "Ẩn geo khác",
|
||||
};
|
||||
|
||||
function buildStepActionEntries(step: ReplayStep): StepActionEntry[] {
|
||||
@@ -1070,9 +1163,15 @@ function buildNarrativeActionEntry(
|
||||
case "set_title":
|
||||
summary = summarizeValue(params[0], "Tiêu đề trống");
|
||||
break;
|
||||
case "clear_title":
|
||||
summary = "title=null";
|
||||
break;
|
||||
case "set_descriptions":
|
||||
summary = summarizeValue(params[0], "Mô tả trống");
|
||||
break;
|
||||
case "clear_descriptions":
|
||||
summary = "descriptions=null";
|
||||
break;
|
||||
case "show_dialog_box":
|
||||
summary = [
|
||||
`speaker=${summarizeValue(params[3], "ẩn danh")}`,
|
||||
@@ -1080,15 +1179,24 @@ function buildNarrativeActionEntry(
|
||||
`text=${summarizeValue(params[1], "trống")}`,
|
||||
].join(" | ");
|
||||
break;
|
||||
case "clear_dialog_box":
|
||||
summary = "dialog=null";
|
||||
break;
|
||||
case "display_historical_image":
|
||||
summary = [
|
||||
`url=${summarizeValue(params[0], "trống")}`,
|
||||
`caption=${summarizeValue(params[1], "trống")}`,
|
||||
].join(" | ");
|
||||
break;
|
||||
case "clear_historical_image":
|
||||
summary = "image=null";
|
||||
break;
|
||||
case "set_step_subtitle":
|
||||
summary = summarizeValue(params[0], "Ẩn subtitle");
|
||||
break;
|
||||
case "clear_step_subtitle":
|
||||
summary = "subtitle=null";
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1129,6 +1237,9 @@ function buildMapActionEntry(
|
||||
case "hide_labels":
|
||||
summary = "visible=false";
|
||||
break;
|
||||
case "show_all_geometries":
|
||||
summary = "hidden_ids=[]";
|
||||
break;
|
||||
case "set_camera_view":
|
||||
summary = summarizeCameraViewValue(params[0]);
|
||||
break;
|
||||
@@ -1248,8 +1359,7 @@ function buildGeoActionEntry(
|
||||
break;
|
||||
case "dim_other_geometries":
|
||||
summary = [
|
||||
`focus=${summarizeGeometryIdsValue(params[0])}`,
|
||||
`other_opacity=${summarizeValue(params[1], "mặc định")}`,
|
||||
`keep=${summarizeGeometryIdsValue(params[0])}`,
|
||||
].join(" | ");
|
||||
break;
|
||||
}
|
||||
@@ -1278,6 +1388,8 @@ function buildUiActionEntry(
|
||||
|
||||
if (option === "timeline" || option === "layer_panel" || option === "wiki_panel" || option === "zoom_panel") {
|
||||
summary = `visible=${Boolean(params[0]) ? "true" : "false"}`;
|
||||
} else if (option === "close_wiki_panel") {
|
||||
summary = "visible=false | active_wiki=null";
|
||||
} else if (option === "wiki") {
|
||||
summary = `wiki_id=${summarizeValue(params[0], "trống")}`;
|
||||
} else if (option === "toast") {
|
||||
@@ -1342,6 +1454,7 @@ function normalizeUiOptionValue(value: unknown): UIOptionName | null {
|
||||
case "timeline":
|
||||
case "layer_panel":
|
||||
case "wiki_panel":
|
||||
case "close_wiki_panel":
|
||||
case "zoom_panel":
|
||||
case "wiki":
|
||||
case "toast":
|
||||
|
||||
Reference in New Issue
Block a user