update: add dialogue preview

This commit is contained in:
Tom Boullay
2026-05-11 12:48:59 +02:00
parent 3791ab71cd
commit 9f5c105c1b
3 changed files with 68 additions and 2 deletions
@@ -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"
+12
View File
@@ -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
View File
@@ -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>
); );
} }