update: add dialogue preview
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Plus, RefreshCw, Save, Trash2 } from "lucide-react";
|
import { Play, Plus, RefreshCw, Save, Trash2 } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
DialogueDefinition,
|
DialogueDefinition,
|
||||||
DialogueManifest,
|
DialogueManifest,
|
||||||
DialogueVoiceId,
|
DialogueVoiceId,
|
||||||
} from "@/types/dialogues/dialogues";
|
} from "@/types/dialogues/dialogues";
|
||||||
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
||||||
|
import { playDialogueById } from "@/utils/dialogues/playDialogue";
|
||||||
|
|
||||||
const DEFAULT_VOICE: DialogueVoiceId = "narrateur";
|
const DEFAULT_VOICE: DialogueVoiceId = "narrateur";
|
||||||
type DialoguePatch = Partial<Omit<DialogueDefinition, "timecode">> & {
|
type DialoguePatch = Partial<Omit<DialogueDefinition, "timecode">> & {
|
||||||
@@ -89,10 +90,12 @@ function getPatchedDialogue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function EditorDialogueManifestPanel(): React.JSX.Element {
|
export function EditorDialogueManifestPanel(): React.JSX.Element {
|
||||||
|
const previewAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
const [manifest, setManifest] = useState<DialogueManifest | null>(null);
|
const [manifest, setManifest] = useState<DialogueManifest | null>(null);
|
||||||
const [selectedDialogueId, setSelectedDialogueId] = useState("");
|
const [selectedDialogueId, setSelectedDialogueId] = useState("");
|
||||||
const [status, setStatus] = useState("Chargement du manifeste...");
|
const [status, setStatus] = useState("Chargement du manifeste...");
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isPreviewing, setIsPreviewing] = useState(false);
|
||||||
const errors = getManifestErrors(manifest);
|
const errors = getManifestErrors(manifest);
|
||||||
const selectedDialogue =
|
const selectedDialogue =
|
||||||
manifest?.dialogues.find(
|
manifest?.dialogues.find(
|
||||||
@@ -184,6 +187,43 @@ export function EditorDialogueManifestPanel(): React.JSX.Element {
|
|||||||
setSelectedDialogueId(nextId);
|
setSelectedDialogueId(nextId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handlePreviewDialogue(): Promise<void> {
|
||||||
|
if (!manifest || !selectedDialogue) return;
|
||||||
|
if (errors.length > 0) {
|
||||||
|
setStatus("Corrige les erreurs avant de lancer la preview.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previewAudioRef.current?.pause();
|
||||||
|
previewAudioRef.current = null;
|
||||||
|
setIsPreviewing(true);
|
||||||
|
setStatus(`Preview dialogue: ${selectedDialogue.id}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const audio = await playDialogueById(manifest, selectedDialogue.id);
|
||||||
|
previewAudioRef.current = audio;
|
||||||
|
|
||||||
|
if (!audio) {
|
||||||
|
setStatus("Dialogue introuvable pour la preview.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFinish = (): void => {
|
||||||
|
audio.removeEventListener("ended", handleFinish);
|
||||||
|
audio.removeEventListener("pause", handleFinish);
|
||||||
|
if (previewAudioRef.current === audio) previewAudioRef.current = null;
|
||||||
|
setIsPreviewing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.addEventListener("ended", handleFinish);
|
||||||
|
audio.addEventListener("pause", handleFinish);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : "Erreur inconnue";
|
||||||
|
setStatus(message);
|
||||||
|
setIsPreviewing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
||||||
@@ -209,6 +249,8 @@ export function EditorDialogueManifestPanel(): React.JSX.Element {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
|
previewAudioRef.current?.pause();
|
||||||
|
previewAudioRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -332,6 +374,16 @@ export function EditorDialogueManifestPanel(): React.JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="editor-dialogue-manifest-preview"
|
||||||
|
type="button"
|
||||||
|
disabled={errors.length > 0 || isPreviewing}
|
||||||
|
onClick={() => void handlePreviewDialogue()}
|
||||||
|
>
|
||||||
|
<Play size={14} aria-hidden="true" />
|
||||||
|
{isPreviewing ? "Playing..." : "Preview dialogue"}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="editor-dialogue-manifest-delete"
|
className="editor-dialogue-manifest-delete"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1731,6 +1731,7 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-dialogue-manifest-actions button,
|
.editor-dialogue-manifest-actions button,
|
||||||
|
.editor-dialogue-manifest-preview,
|
||||||
.editor-dialogue-manifest-delete {
|
.editor-dialogue-manifest-delete {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1747,6 +1748,7 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-dialogue-manifest-actions button:hover,
|
.editor-dialogue-manifest-actions button:hover,
|
||||||
|
.editor-dialogue-manifest-preview:hover,
|
||||||
.editor-dialogue-manifest-delete:hover {
|
.editor-dialogue-manifest-delete:hover {
|
||||||
border-color: #ffffff;
|
border-color: #ffffff;
|
||||||
background: #202020;
|
background: #202020;
|
||||||
@@ -1757,6 +1759,11 @@ canvas {
|
|||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-dialogue-manifest-preview:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-dialogue-manifest-select,
|
.editor-dialogue-manifest-select,
|
||||||
.editor-dialogue-manifest-form label {
|
.editor-dialogue-manifest-form label {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1801,6 +1808,11 @@ canvas {
|
|||||||
color: #fca5a5;
|
color: #fca5a5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-dialogue-manifest-preview {
|
||||||
|
border-color: rgba(125, 211, 252, 0.24);
|
||||||
|
color: #bae6fd;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-dialogue-manifest-status {
|
.editor-dialogue-manifest-status {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #8d8d8d;
|
color: #8d8d8d;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useState } from "react";
|
|||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { EditorControls } from "@/components/editor/EditorControls";
|
import { EditorControls } from "@/components/editor/EditorControls";
|
||||||
import { EditorScene } from "@/components/editor/scene/EditorScene";
|
import { EditorScene } from "@/components/editor/scene/EditorScene";
|
||||||
|
import { Subtitles } from "@/components/ui/Subtitles";
|
||||||
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
|
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
|
||||||
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
|
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
|
||||||
import type { MapNode, SceneData, TransformMode } from "@/types/editor/editor";
|
import type { MapNode, SceneData, TransformMode } from "@/types/editor/editor";
|
||||||
@@ -196,6 +197,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
isPlayerMode={isPlayerMode}
|
isPlayerMode={isPlayerMode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Subtitles />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user