feat: update replay preview logic, refactor narrative editor state management, and improve overlay UI styling
Build and Release / release (push) Successful in 40s
Build and Release / release (push) Successful in 40s
This commit is contained in:
@@ -1108,6 +1108,14 @@ function EditorPageContent() {
|
||||
timelineFilterEnabled,
|
||||
]);
|
||||
|
||||
const handleEnterPreviewClick = useCallback(() => {
|
||||
if (mode === "replay") {
|
||||
openReplayPreview("start");
|
||||
} else {
|
||||
openViewerPreview();
|
||||
}
|
||||
}, [mode, openReplayPreview, openViewerPreview]);
|
||||
|
||||
const viewerPreviewSelectedReplay = useMemo(() => {
|
||||
if (!isViewerPreviewMode || !selectedFeatureIds.length) return null;
|
||||
const selectedGeometryId = String(selectedFeatureIds[0] ?? "").trim();
|
||||
@@ -1212,9 +1220,9 @@ function EditorPageContent() {
|
||||
}
|
||||
|
||||
if (m === "replay" && featureId) {
|
||||
// QUY TẮC: Geo chọn đầu tiên là geo main.
|
||||
const finalSelectedIds = Array.from(new Set([...selectedFeatureIds, featureId]));
|
||||
const triggerId = selectedFeatureIds.length > 0 ? selectedFeatureIds[0] : featureId;
|
||||
// Sử dụng chính geo được click chuột phải làm main replay geometry
|
||||
const triggerId = featureId;
|
||||
const finalSelectedIds = Array.from(new Set([featureId, ...selectedFeatureIds]));
|
||||
|
||||
setReplayFeatureId(triggerId);
|
||||
setReplaySelection({ stageId: null, stepIndex: null });
|
||||
@@ -2682,7 +2690,7 @@ function EditorPageContent() {
|
||||
localFeatureIds={localFeatureIds}
|
||||
showViewportControls={!isReplayPreviewMode || replayPreview.zoomPanelVisible}
|
||||
isPreviewMode={isAnyPreviewMode}
|
||||
onEnterPreview={openViewerPreview}
|
||||
onEnterPreview={handleEnterPreviewClick}
|
||||
onExitPreview={isReplayPreviewMode ? exitReplayPreview : exitViewerPreview}
|
||||
onPlayPreviewReplay={viewerPreviewSelectedReplay ? handleMapPlayPreviewReplay : undefined}
|
||||
viewMode={viewMode}
|
||||
|
||||
@@ -520,6 +520,7 @@ export default function ReplayEffectsSidebar({
|
||||
{selectedStage && selectedStep && selectedStepIndex != null ? (
|
||||
<>
|
||||
<ActionGroupEditor
|
||||
resetKey={`narrative-${selectedStage.id}-${selectedStepIndex}`}
|
||||
title="Narrative"
|
||||
groupLabel={`Replay: cập nhật narrative step ${selectedStepIndex + 1} của stage #${selectedStage.id}`}
|
||||
actions={selectedStep.use_narrow_function}
|
||||
@@ -1144,6 +1145,7 @@ function ActionGroupEditor<T extends string>({
|
||||
emptyOptionLabel,
|
||||
onUpdateActions,
|
||||
onLinkClick,
|
||||
resetKey,
|
||||
}: {
|
||||
title: string;
|
||||
groupLabel: string;
|
||||
@@ -1155,6 +1157,7 @@ function ActionGroupEditor<T extends string>({
|
||||
emptyOptionLabel?: string;
|
||||
onUpdateActions: (nextActions: ReplayAction<T>[], label: string) => void;
|
||||
onLinkClick?: (quill: any) => void;
|
||||
resetKey?: string;
|
||||
}) {
|
||||
const functionNames = useMemo(() => Object.keys(definitions) as T[], [definitions]);
|
||||
const [composerFunctionName, setComposerFunctionName] = useState<T | "">(
|
||||
@@ -1167,12 +1170,15 @@ function ActionGroupEditor<T extends string>({
|
||||
)
|
||||
);
|
||||
|
||||
const lastResetKeyRef = useRef<string | undefined>(undefined);
|
||||
const lastLoadedActionsRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (JSON.stringify(actions) === JSON.stringify(lastLoadedActionsRef.current)) {
|
||||
const resetKeyChanged = resetKey !== lastResetKeyRef.current;
|
||||
if (!resetKeyChanged && JSON.stringify(actions) === JSON.stringify(lastLoadedActionsRef.current)) {
|
||||
return;
|
||||
}
|
||||
lastResetKeyRef.current = resetKey;
|
||||
lastLoadedActionsRef.current = actions;
|
||||
|
||||
if (actions.length > 0) {
|
||||
@@ -1187,7 +1193,7 @@ function ActionGroupEditor<T extends string>({
|
||||
setComposerFunctionName(defaultFun);
|
||||
setComposerDraftValues(buildActionComposerDraft(definitions, defaultFun));
|
||||
}
|
||||
}, [actions, definitions, createOnSelect, functionNames]);
|
||||
}, [actions, definitions, createOnSelect, functionNames, resetKey]);
|
||||
|
||||
const composerDefinition = composerFunctionName
|
||||
? definitions[composerFunctionName]
|
||||
@@ -1343,6 +1349,29 @@ function FieldInput({
|
||||
onChange: (nextValue: ActionValue) => void;
|
||||
onLinkClick?: (quill: any) => void;
|
||||
}) {
|
||||
const onLinkClickRef = useRef(onLinkClick);
|
||||
useEffect(() => {
|
||||
onLinkClickRef.current = onLinkClick;
|
||||
}, [onLinkClick]);
|
||||
|
||||
const quillModules = useMemo(() => {
|
||||
return {
|
||||
toolbar: {
|
||||
container: [
|
||||
["bold", "italic", "underline", "strike"],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
["link"],
|
||||
["clean"],
|
||||
],
|
||||
handlers: {
|
||||
link: function (this: { quill?: any }) {
|
||||
onLinkClickRef.current?.(this?.quill);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
const baseLabel = (
|
||||
<div style={{ fontSize: 12, color: "#cbd5e1", fontWeight: 700 }}>
|
||||
{field.label}
|
||||
@@ -1358,21 +1387,7 @@ function FieldInput({
|
||||
theme="snow"
|
||||
value={asString(value)}
|
||||
onChange={(content: string) => onChange(content)}
|
||||
modules={{
|
||||
toolbar: {
|
||||
container: [
|
||||
["bold", "italic", "underline", "strike"],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
["link"],
|
||||
["clean"],
|
||||
],
|
||||
handlers: {
|
||||
link: function (this: { quill?: any }) {
|
||||
onLinkClick?.(this?.quill);
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
modules={quillModules}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -93,53 +93,69 @@ export default function ReplayPreviewOverlay({
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 18,
|
||||
left: 88,
|
||||
right: rightOffset,
|
||||
bottom: 96,
|
||||
borderRadius: 20,
|
||||
overflow: "hidden",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
background: "rgba(11, 18, 32, 0.85)",
|
||||
backdropFilter: "blur(12px)",
|
||||
boxShadow: "0 16px 44px rgba(2, 6, 23, 0.42)",
|
||||
pointerEvents: "auto",
|
||||
pointerEvents: "none",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxHeight: "calc(100vh - 180px)",
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
{dialog.image_url?.trim() ? (
|
||||
<img
|
||||
src={dialog.image_url}
|
||||
alt="Historical"
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "block",
|
||||
maxHeight: 140,
|
||||
objectFit: "cover",
|
||||
background: "#020617",
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{dialog.text?.trim() ? (
|
||||
<div
|
||||
className="uhm-replay-dialog-content"
|
||||
style={{
|
||||
padding: "16px",
|
||||
color: "#f8fafc",
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.6",
|
||||
overflowY: "auto",
|
||||
maxHeight: dialog.image_url?.trim() ? "180px" : "140px",
|
||||
minHeight: 0,
|
||||
background: "transparent",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: dialog.text }}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
style={{
|
||||
width: "min(640px, 100%)",
|
||||
borderRadius: 20,
|
||||
overflow: "hidden",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
background: "rgba(11, 18, 32, 0.85)",
|
||||
backdropFilter: "blur(12px)",
|
||||
boxShadow: "0 16px 44px rgba(2, 6, 23, 0.42)",
|
||||
pointerEvents: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxHeight: "calc(100vh - 180px)",
|
||||
}}
|
||||
>
|
||||
{dialog.image_url?.trim() ? (
|
||||
<img
|
||||
src={dialog.image_url}
|
||||
alt="Historical"
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "block",
|
||||
maxHeight: 140,
|
||||
objectFit: "cover",
|
||||
background: "#020617",
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{dialog.text?.trim() ? (
|
||||
<div
|
||||
className="uhm-replay-dialog-content"
|
||||
style={{
|
||||
padding: "16px",
|
||||
color: "#f8fafc",
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.6",
|
||||
overflowY: "auto",
|
||||
maxHeight: dialog.image_url?.trim() ? "180px" : "140px",
|
||||
minHeight: 0,
|
||||
background: "transparent",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: dialog.text }}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<style jsx>{`
|
||||
.uhm-replay-dialog-content::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.uhm-replay-dialog-content {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.uhm-replay-dialog-content :global(p) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,6 @@ export type UIOptionName =
|
||||
| "layer_panel"
|
||||
| "zoom_panel"
|
||||
| "wiki"
|
||||
| "toast";
|
||||
|
||||
export type MapFunctionName =
|
||||
| "set_camera_view"
|
||||
@@ -226,7 +225,6 @@ export type ReplayUiParamTupleDocs = {
|
||||
layer_panel: [visible: boolean];
|
||||
zoom_panel: [visible: boolean];
|
||||
wiki: [wiki_id: string | null];
|
||||
toast: [message: string];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user