Feat/env-manager #1

Merged
math-pixel merged 46 commits from feat/env-manager into develop 2026-05-11 15:23:32 +00:00
3 changed files with 235 additions and 0 deletions
Showing only changes of commit b4c49d87d8 - Show all commits
+3
View File
@@ -12,6 +12,7 @@ import {
Save,
Undo2,
} from "lucide-react";
import { EditorSrtPanel } from "@/components/editor/EditorSrtPanel";
import type { MapNode, TransformMode } from "@/types/editor/editor";
interface EditorControlsProps {
@@ -236,6 +237,8 @@ export function EditorControls({
: `Selected node ${selectedNodeIndex + 1} raw lines`}
</div>
</section>
<EditorSrtPanel />
</aside>
</>
);
+161
View File
@@ -0,0 +1,161 @@
import { useEffect, useState } from "react";
import { Download, RefreshCw } from "lucide-react";
import type { SubtitleLanguage } from "@/managers/stores/useSettingsStore";
import type {
DialogueSpeaker,
DialogueVoiceId,
} from "@/types/dialogues/dialogues";
interface SrtVoiceOption {
id: DialogueVoiceId;
label: DialogueSpeaker;
}
const SRT_VOICES: SrtVoiceOption[] = [
{ id: "narrateur", label: "Narrateur" },
{ id: "fermier", label: "Fermier" },
{ id: "leonie", label: "Leonie" },
];
const DEFAULT_SRT_VOICE: SrtVoiceOption = {
id: "narrateur",
label: "Narrateur",
};
const SRT_LANGUAGES: SubtitleLanguage[] = ["fr", "en"];
function getSrtPath(
voice: DialogueVoiceId,
language: SubtitleLanguage,
): string {
return `/sounds/dialogue/subtitles/${language}/${voice}.srt`;
}
function createEmptySrtTemplate(speaker: DialogueSpeaker): string {
return `1\n00:00:00,000 --> 00:00:02,000\n${speaker}: Nouveau sous-titre\n`;
}
function downloadSrtFile(
voice: DialogueVoiceId,
language: SubtitleLanguage,
content: string,
): void {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${voice}.${language}.srt`;
link.click();
window.setTimeout(() => URL.revokeObjectURL(url), 0);
}
export function EditorSrtPanel(): React.JSX.Element {
const [voice, setVoice] = useState<DialogueVoiceId>("narrateur");
const [language, setLanguage] = useState<SubtitleLanguage>("fr");
const [content, setContent] = useState("");
const [status, setStatus] = useState("Chargement du SRT...");
const selectedVoice =
SRT_VOICES.find((item) => item.id === voice) ?? DEFAULT_SRT_VOICE;
useEffect(() => {
let mounted = true;
const srtPath = getSrtPath(voice, language);
void fetch(srtPath)
.then(async (response) => {
if (!mounted) return;
if (!response.ok) {
setContent(createEmptySrtTemplate(selectedVoice.label));
setStatus("Fichier absent, template local cree");
return;
}
setContent(await response.text());
setStatus(`Charge depuis ${srtPath}`);
})
.catch(() => {
if (!mounted) return;
setContent(createEmptySrtTemplate(selectedVoice.label));
setStatus("Erreur de chargement, template local cree");
});
return () => {
mounted = false;
};
}, [language, selectedVoice.label, voice]);
return (
<section className="editor-srt-section" aria-labelledby="srt-heading">
<div className="editor-section-heading">
<h3 id="srt-heading">SRT</h3>
<span>{language.toUpperCase()}</span>
</div>
<div className="editor-srt-controls">
<label>
Voix
<select
value={voice}
onChange={(event) =>
setVoice(event.target.value as DialogueVoiceId)
}
>
{SRT_VOICES.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
</select>
</label>
<label>
Langue
<select
value={language}
onChange={(event) =>
setLanguage(event.target.value as SubtitleLanguage)
}
>
{SRT_LANGUAGES.map((item) => (
<option key={item} value={item}>
{item.toUpperCase()}
</option>
))}
</select>
</label>
</div>
<textarea
className="editor-srt-textarea"
value={content}
spellCheck={false}
onChange={(event) => setContent(event.target.value)}
onKeyDown={(event) => event.stopPropagation()}
aria-label="SRT content"
/>
<div className="editor-srt-actions">
<button
className="editor-action-button"
type="button"
onClick={() =>
setContent(createEmptySrtTemplate(selectedVoice.label))
}
>
<RefreshCw size={15} aria-hidden="true" />
Template
</button>
<button
className="editor-action-button editor-action-button-primary"
type="button"
onClick={() => downloadSrtFile(voice, language, content)}
>
<Download size={15} aria-hidden="true" />
Export SRT
</button>
</div>
<p className="editor-srt-status">{status}</p>
</section>
);
}
+71
View File
@@ -1395,6 +1395,77 @@ canvas {
font-size: 0.74rem;
}
/* Editor SRT panel */
.editor-srt-section {
display: grid;
gap: 10px;
padding: 14px 12px 12px;
border-top: 1px solid rgba(255, 255, 255, 0.09);
}
.editor-srt-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.editor-srt-controls label {
display: grid;
gap: 5px;
color: #8d8d8d;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.editor-srt-controls select {
width: 100%;
padding: 9px 10px;
border: 1px solid #242424;
border-radius: 12px;
background: #101010;
color: #f2f2f2;
}
.editor-srt-textarea {
width: 100%;
min-height: 260px;
resize: vertical;
box-sizing: border-box;
padding: 12px;
border: 1px solid #1f1f1f;
border-radius: 16px;
background: #050505;
color: #d7d7d7;
font-family: "SFMono-Regular", "Courier New", monospace;
font-size: 0.72rem;
line-height: 1.55;
}
.editor-srt-textarea:focus,
.editor-srt-controls select:focus {
border-color: #ffffff;
outline: none;
}
.editor-srt-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.editor-srt-actions .editor-action-button + .editor-action-button {
margin-top: 0;
}
.editor-srt-status {
margin: 0;
color: #8d8d8d;
font-size: 0.72rem;
line-height: 1.4;
}
/* Editor responsive layout */
@media (max-width: 768px) {
.editor-error h2 {