book_generator/static/js/main.js

1133 lines
No EOL
36 KiB
JavaScript

let enable_auto_update = true;
let previewTimeout = null;
let isScrolling = false;
// Initialisation de CodeMirror
const editor = CodeMirror.fromTextArea(document.getElementById('editor-content'), {
mode: 'org',
theme: 'monokai',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 4,
tabSize: 4,
indentWithTabs: false,
extraKeys: {
"Tab": "indentMore",
"Shift-Tab": "indentLess"
}
});
// Gestion du thème
const themeSwitch = document.getElementById('theme-switch');
const htmlElement = document.documentElement;
// Charger le thème sauvegardé
const savedTheme = localStorage.getItem('theme') || 'light';
htmlElement.setAttribute('data-theme', savedTheme);
themeSwitch.checked = savedTheme === 'dark';
// Gestion de l'objectif de mots
const wordGoalInput = document.getElementById('word-goal');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
// Charger l'objectif sauvegardé
const savedGoal = localStorage.getItem('wordGoal') || '400';
wordGoalInput.value = savedGoal;
// Gestion des filtres
const showTitlesCheckbox = document.getElementById('show-titles');
const showRegularTitlesCheckbox = document.getElementById('show-regular-titles');
const showCommentsCheckbox = document.getElementById('show-comments');
// Écouteurs d'événements pour les filtres
showTitlesCheckbox.addEventListener('change', updatePreview);
showRegularTitlesCheckbox.addEventListener('change', updatePreview);
showCommentsCheckbox.addEventListener('change', updatePreview);
// Mettre à jour la progression
function updateProgress(currentWords, goal) {
const percentage = Math.min(Math.round((currentWords / goal) * 100), 100);
progressBar.style.width = `${percentage}%`;
progressText.textContent = `${percentage}% de l'objectif`;
// Changer la couleur de la barre selon la progression
if (percentage >= 100) {
progressBar.style.backgroundColor = '#198754'; // Vert
} else if (percentage >= 75) {
progressBar.style.backgroundColor = '#0dcaf0'; // Bleu clair
} else if (percentage >= 50) {
progressBar.style.backgroundColor = '#0d6efd'; // Bleu
} else if (percentage >= 25) {
progressBar.style.backgroundColor = '#ffc107'; // Jaune
} else {
progressBar.style.backgroundColor = '#dc3545'; // Rouge
}
}
// Écouteur d'événements pour le changement d'objectif
wordGoalInput.addEventListener('change', (e) => {
const goal = parseInt(e.target.value) || 400;
localStorage.setItem('wordGoal', goal);
updateProgress(parseInt(document.getElementById('words-today').textContent), goal);
});
// Écouteur d'événements pour le switch de thème
themeSwitch.addEventListener('change', (e) => {
const theme = e.target.checked ? 'dark' : 'light';
htmlElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
});
// Fonction pour extraire les titres du texte org
function extractHeadings(text) {
const headings = [];
const lines = text.split('\n');
let currentLine = 0;
while (currentLine < lines.length) {
const line = lines[currentLine];
const match = line.match(/^(\*+)\s+(.+)$/);
if (match) {
const level = match[1].length;
const title = match[2];
const lineNumber = currentLine;
// Ajouter le titre à la liste des titres
headings.push({
level,
title,
lineNumber,
type: 'heading'
});
}
currentLine++;
}
return headings;
}
// Fonction pour créer la table des matières
function createTableOfContents(headings) {
const toc = document.createElement('div');
toc.className = 'table-of-contents';
toc.innerHTML = '<h5>Table des matières</h5>';
const ul = document.createElement('ul');
ul.className = 'toc-list';
headings.forEach(heading => {
const li = document.createElement('li');
li.className = `toc-item level-${heading.level}`;
const link = document.createElement('a');
link.href = '#';
link.textContent = heading.title;
link.addEventListener('click', (e) => {
e.preventDefault();
scrollToHeading(heading.lineNumber);
});
// Ajouter le compteur de mots si on est dans le livre
const editorTitle = document.querySelector('.editor h2').textContent;
if (editorTitle === 'Livre') {
const wordCount = countWordsInSection(heading.lineNumber);
const wordGoal = parseInt(document.getElementById('word-goal').value);
const progress = Math.min(Math.round((wordCount / wordGoal) * 100), 100);
const countSpan = document.createElement('span');
countSpan.className = 'word-count-badge';
countSpan.textContent = `${wordCount}/${wordGoal} mots (${progress}%)`;
countSpan.style.marginLeft = '10px';
countSpan.style.fontSize = '0.8em';
countSpan.style.color = progress >= 100 ? '#198754' : progress >= 75 ? '#0dcaf0' : progress >= 50 ? '#0d6efd' : progress >= 25 ? '#ffc107' : '#dc3545';
link.appendChild(countSpan);
}
li.appendChild(link);
ul.appendChild(li);
});
toc.appendChild(ul);
return toc;
}
// Fonction pour compter les mots dans une section
function countWordsInSection(startLine) {
const lines = editor.getValue().split('\n');
let wordCount = 0;
let currentLine = startLine + 1;
while (currentLine < lines.length && !lines[currentLine].startsWith('*')) {
const line = lines[currentLine].trim();
if (line && !line.startsWith('#')) {
wordCount += line.split(/\s+/).length;
}
currentLine++;
}
return wordCount;
}
// Fonction pour créer la table des matières des personnages
function createCharactersTOC(characters) {
const toc = document.createElement('div');
toc.className = 'characters-toc';
toc.innerHTML = '<h6>Table des matières des personnages</h6>';
const ul = document.createElement('ul');
ul.className = 'toc-list';
characters.forEach(character => {
const li = document.createElement('li');
li.className = 'toc-item level-2'; // Les personnages sont toujours de niveau 2
const link = document.createElement('a');
link.href = '#';
link.textContent = character.name;
link.addEventListener('click', async (e) => {
e.preventDefault();
await loadCharactersFile();
// Trouver la ligne du personnage dans le fichier
const content = editor.getValue();
const lines = content.split('\n');
let targetLine = 0;
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim() === `** ${character.name}`) {
targetLine = i;
break;
}
}
// Positionner le curseur sur le titre du personnage
editor.setCursor({ line: targetLine, ch: 0 });
editor.focus();
});
li.appendChild(link);
ul.appendChild(li);
});
toc.appendChild(ul);
return toc;
}
// Fonction pour charger un personnage
async function loadCharacter(name) {
try {
const response = await fetch('/get_character', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `name=${encodeURIComponent(name)}`
});
if (response.ok) {
const data = await response.json();
editor.setValue(data.content);
document.querySelector('.editor h2').textContent = `Éditeur de Personnage: ${name}`;
updatePreview();
} else {
alert('Erreur lors du chargement du personnage');
}
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors du chargement du personnage');
}
}
// Fonction pour charger une intrigue
async function loadPlot(name) {
try {
const response = await fetch('/get_plot', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `name=${encodeURIComponent(name)}`
});
if (response.ok) {
const data = await response.json();
editor.setValue(data.content);
document.querySelector('.editor h2').textContent = `Éditeur d'Intrigue: ${name}`;
updatePreview();
} else {
alert('Erreur lors du chargement de l\'intrigue');
}
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors du chargement de l\'intrigue');
}
}
// Fonction pour charger le livre
async function loadBook() {
try {
const response = await fetch('/get_book');
if (response.ok) {
const data = await response.json();
editor.setValue(data.content);
document.querySelector('.editor h2').textContent = 'Livre';
updatePreview();
} else {
alert('Erreur lors du chargement du livre');
}
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors du chargement du livre');
}
}
// Fonction pour charger le fichier intrigues.org complet
async function loadPlotsFile() {
try {
const response = await fetch('/get_plots_file');
if (response.ok) {
const data = await response.json();
editor.setValue(data.content);
document.querySelector('.editor h2').textContent = 'Intrigues';
updatePreview();
} else {
alert('Erreur lors du chargement du fichier des intrigues');
}
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors du chargement du fichier des intrigues');
}
}
// Fonction pour charger le fichier personnages.org complet
async function loadCharactersFile() {
try {
const response = await fetch('/get_characters_file');
if (response.ok) {
const data = await response.json();
editor.setValue(data.content);
document.querySelector('.editor h2').textContent = 'Personnages';
updatePreview();
} else {
alert('Erreur lors du chargement du fichier des personnages');
}
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors du chargement du fichier des personnages');
}
}
// Fonction pour faire défiler jusqu'à un titre
function scrollToHeading(lineNumber) {
const editor = document.getElementById('editor-content');
const preview = document.getElementById('preview-content');
const lines = editor.value.split('\n');
// Calculer la position approximative dans le texte
let position = 0;
for (let i = 0; i < lineNumber; i++) {
position += lines[i].length + 1; // +1 pour le retour à la ligne
}
// Faire défiler l'éditeur
editor.focus();
editor.setSelectionRange(position, position);
editor.scrollTop = editor.scrollHeight * (lineNumber / lines.length);
// Faire défiler la prévisualisation
const headings = preview.getElementsByClassName('org-heading');
for (const heading of headings) {
if (heading.textContent === lines[lineNumber].replace(/^\*+\s+/, '')) {
heading.scrollIntoView({ behavior: 'smooth', block: 'start' });
break;
}
}
}
// Fonction pour convertir le texte org en HTML
function orgToHtml(text) {
// Supprimer les commentaires avant toute autre conversion
text = text.replace(/^#\+begin_comment[\s\S]*?#\+end_comment$/gm, '');
text = text.replace(/^# .*$/gm, '');
// Ignorer les lignes de propriétés et d'export
text = text.split('\n').filter(line => {
return !line.match(/^:PROPERTIES:|^:ID:|^:END:|^#\+BEGIN_EXPORT|^#\+END_EXPORT|^#\+[A-Z_]+:/);
}).join('\n');
// Récupérer la liste des personnages
const characters = JSON.parse(document.getElementById('characters-data').textContent);
const characterNames = characters.map(char => char.name);
// Conversion des liens vers les personnages et intrigues
text = text.replace(/\[\[(.*?)\]\]/g, (match, content) => {
const [type, name] = content.split(':');
if (type === 'character') {
return `<a href="#" class="character-link" data-type="character" data-name="${name}">${name}</a>`;
} else if (type === 'plot') {
return `<a href="#" class="plot-link" data-type="plot" data-name="${name}">${name}</a>`;
}
return match;
});
// Conversion des noms de personnages en liens
characterNames.forEach(name => {
// Créer une expression régulière qui correspond au nom du personnage
// mais pas s'il est déjà dans un lien ou dans un titre
const regex = new RegExp(`(?<!<a[^>]*>)(?<!\\*+\\s)${name}(?!</a>)(?!\\s*:tit[rl]e:)`, 'gi');
text = text.replace(regex, `<a href="#" class="character-link" data-type="character" data-name="${name}">${name}</a>`);
});
// Conversion des titres avec :titre:
text = text.replace(/^\*+ (.*?):tit[rl]e:(.*)$/gm, (match, content, rest) => {
const level = match.match(/^\*+/)[0].length;
return `<h${level} class="org-heading title-section">${content}${rest}</h${level}>`;
});
// Conversion des titres normaux (qui ne contiennent pas :titre:)
text = text.replace(/^\*+ (?!.*:tit[rl]e:)(.*)$/gm, (match, content) => {
const level = match.match(/^\*+/)[0].length;
return `<h${level} class="org-heading regular-title">${content}</h${level}>`;
});
// Conversion des listes
text = text.replace(/^- (.*)$/gm, '<li>$1</li>');
text = text.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
// Conversion des citations
text = text.replace(/^#\+BEGIN_QUOTE\n(.*?)\n#\+END_QUOTE$/gs, '<blockquote>$1</blockquote>');
// Conversion du code
text = text.replace(/^#\+BEGIN_SRC.*\n(.*?)\n#\+END_SRC$/gs, '<pre><code>$1</code></pre>');
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
// Conversion des paragraphes
text = text.split('\n\n').map(para => {
if (!para.trim()) return '';
if (!para.match(/^<[hul]|^<blockquote|^<pre/)) {
return `<p>${para}</p>`;
}
return para;
}).join('\n');
return text;
}
// Fonction pour mettre à jour la prévisualisation
function updatePreview() {
if (!enable_auto_update) return;
const content = editor.getValue();
const previewContent = document.getElementById('preview-content');
previewContent.innerHTML = orgToHtml(content);
// Appliquer les filtres et compter les éléments
const titleSections = previewContent.getElementsByClassName('title-section');
const regularTitles = previewContent.getElementsByClassName('regular-title');
const commentSections = previewContent.getElementsByClassName('comment-section');
// Mettre à jour les compteurs dans les labels
const showTitlesLabel = document.querySelector('label[for="show-titles"]');
const showRegularTitlesLabel = document.querySelector('label[for="show-regular-titles"]');
const showCommentsLabel = document.querySelector('label[for="show-comments"]');
// Supprimer les anciens compteurs s'ils existent
showTitlesLabel.innerHTML = showTitlesLabel.innerHTML.replace(/ \([0-9]+\)$/, '');
showRegularTitlesLabel.innerHTML = showRegularTitlesLabel.innerHTML.replace(/ \([0-9]+\)$/, '');
showCommentsLabel.innerHTML = showCommentsLabel.innerHTML.replace(/ \([0-9]+\)$/, '');
// Ajouter les nouveaux compteurs
if (showTitlesCheckbox.checked) {
showTitlesLabel.innerHTML += ` (${titleSections.length})`;
}
if (showRegularTitlesCheckbox.checked) {
showRegularTitlesLabel.innerHTML += ` (${regularTitles.length})`;
}
if (showCommentsCheckbox.checked) {
showCommentsLabel.innerHTML += ` (${commentSections.length})`;
}
// Appliquer les filtres
Array.from(titleSections).forEach(section => {
section.classList.toggle('hidden', !showTitlesCheckbox.checked);
});
Array.from(regularTitles).forEach(section => {
section.classList.toggle('hidden', !showRegularTitlesCheckbox.checked);
});
Array.from(commentSections).forEach(section => {
section.classList.toggle('hidden', !showCommentsCheckbox.checked);
});
// Mettre à jour la table des matières
const headings = extractHeadings(content);
const tocContainer = document.querySelector('.table-of-contents');
if (tocContainer) {
tocContainer.remove();
}
const sidebar = document.querySelector('.sidebar');
const toc = createTableOfContents(headings);
sidebar.insertBefore(toc, document.querySelector('.word-count'));
// Créer la table des matières des personnages
const characters = JSON.parse(document.getElementById('characters-data').textContent);
const charactersTOC = createCharactersTOC(characters);
const charactersTOCContainer = document.querySelector('.characters-toc');
if (charactersTOCContainer) {
charactersTOCContainer.innerHTML = '';
charactersTOCContainer.appendChild(charactersTOC);
}
}
// Synchronisation du défilement
function syncScroll(source, target) {
if (isScrolling) return;
isScrolling = true;
const sourceScrollPercent = source.scrollTop / (source.scrollHeight - source.clientHeight);
const targetScrollTop = sourceScrollPercent * (target.scrollHeight - target.clientHeight);
target.scrollTop = targetScrollTop;
setTimeout(() => {
isScrolling = false;
}, 100);
}
// Écouteurs d'événements pour la synchronisation du défilement
const preview = document.getElementById('preview-content');
editor.getWrapperElement().addEventListener('scroll', () => {
syncScroll(editor.getWrapperElement(), preview);
});
preview.addEventListener('scroll', () => {
syncScroll(preview, editor.getWrapperElement());
});
// Écouteur d'événements pour la mise à jour automatique de la prévisualisation
editor.on('change', () => {
if (document.getElementById('auto-preview').checked) {
clearTimeout(previewTimeout);
previewTimeout = setTimeout(updatePreview, 500);
}
});
// Écouteur d'événements pour le switch de mise à jour automatique
document.getElementById('auto-preview').addEventListener('change', (e) => {
if (e.target.checked) {
updatePreview();
}
});
// Sauvegarde automatique si activée
if (enable_auto_update) {
setInterval(async () => {
const content = editor.getValue();
const editorTitle = document.querySelector('.editor h2').textContent;
let endpoint = '/update';
let body = `content=${encodeURIComponent(content)}&editor_title=${encodeURIComponent(editorTitle)}`;
// Déterminer le bon endpoint en fonction du titre
if (editorTitle.startsWith('Éditeur de Personnage:')) {
const name = editorTitle.split(':')[1].trim();
endpoint = '/update_character';
body = `name=${encodeURIComponent(name)}&content=${encodeURIComponent(content)}`;
} else if (editorTitle.startsWith('Éditeur d\'Intrigue:')) {
const name = editorTitle.split(':')[1].trim();
endpoint = '/update_plot';
body = `name=${encodeURIComponent(name)}&content=${encodeURIComponent(content)}`;
} else if (editorTitle === 'Personnages') {
endpoint = '/update_characters_file';
} else if (editorTitle === 'Intrigues') {
endpoint = '/update_plots_file';
}
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body
});
const data = await response.json();
if (response.ok) {
if (endpoint === '/update') {
document.getElementById('words-today').textContent = data.words_today;
updateProgress(data.words_today, parseInt(wordGoalInput.value));
}
console.log('Sauvegarde automatique effectuée');
}
} catch (error) {
console.error('Erreur lors de la sauvegarde automatique');
}
}, 10000); // 10 secondes
}
// Mise à jour du contenu
document.getElementById('update-btn').addEventListener('click', async () => {
const content = editor.getValue();
const editorTitle = document.querySelector('.editor h2').textContent;
try {
const response = await fetch('/update', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `content=${encodeURIComponent(content)}&editor_title=${encodeURIComponent(editorTitle)}`
});
const data = await response.json();
if (response.ok) {
if (editorTitle === 'Livre') {
document.getElementById('words-today').textContent = data.words_today;
updateProgress(data.words_today, parseInt(wordGoalInput.value));
}
alert('Contenu mis à jour avec succès !');
}
} catch (error) {
alert('Erreur lors de la mise à jour');
}
});
// Mise à jour automatique du compteur de mots
async function updateWordCount() {
const editorTitle = document.querySelector('.editor h2').textContent;
if (editorTitle !== 'Livre') return;
try {
const response = await fetch('/words_today');
const data = await response.json();
document.getElementById('words-today').textContent = data.words;
updateProgress(data.words, parseInt(wordGoalInput.value));
} catch (error) {
console.error('Erreur lors de la mise à jour du compteur de mots');
}
}
// Mise à jour toutes les 30 secondes
setInterval(updateWordCount, 30000);
// Initialisation du graphique de progression
let progressChart = null;
async function updateProgressChart() {
try {
const response = await fetch('/progress_data');
const data = await response.json();
if (progressChart) {
progressChart.destroy();
}
const ctx = document.getElementById('progressChart').getContext('2d');
progressChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.labels,
datasets: [
{
label: 'Progression (mots)',
data: data.progress,
backgroundColor: 'rgba(13, 110, 253, 0.5)',
borderColor: 'rgba(13, 110, 253, 1)',
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Nombre de mots'
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
return `${context.raw} mots`;
}
}
}
}
}
});
} catch (error) {
console.error('Erreur lors de la mise à jour du graphique:', error);
}
}
// Mise à jour du graphique toutes les 30 secondes
setInterval(updateProgressChart, 30000);
// Initialisation de la prévisualisation, de la progression et du graphique
updatePreview();
updateProgress(parseInt(document.getElementById('words-today').textContent), parseInt(wordGoalInput.value));
updateProgressChart();
// Initialisation des tooltips
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
// Gestion des modales pour les personnages et les intrigues
document.addEventListener('DOMContentLoaded', function () {
// Mise à jour initiale du compteur de mots
updateWordCount();
// Initialisation des tooltips Bootstrap
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Gestion du bouton d'ajout de commentaire
const addCommentBtn = document.getElementById('add-comment-btn');
if (addCommentBtn) {
addCommentBtn.addEventListener('click', addCommentBlock);
}
// Gestion des personnages
const characterLinks = document.querySelectorAll('.character-link');
const characterModal = document.getElementById('characterModal');
const characterNameInput = document.getElementById('character-name');
const characterContentInput = document.getElementById('character-content');
const saveCharacterBtn = document.getElementById('save-character');
characterLinks.forEach(link => {
link.addEventListener('click', function (e) {
e.preventDefault();
const name = this.dataset.name;
const content = this.dataset.content;
characterNameInput.value = name;
characterContentInput.value = content;
});
});
saveCharacterBtn.addEventListener('click', async function () {
const name = characterNameInput.value;
const content = characterContentInput.value;
try {
const response = await fetch('/update_character', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `name=${encodeURIComponent(name)}&content=${encodeURIComponent(content)}`
});
if (response.ok) {
// Recharger la page pour mettre à jour la liste
window.location.reload();
} else {
alert('Erreur lors de la sauvegarde du personnage');
}
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors de la sauvegarde du personnage');
}
});
// Gestion des intrigues
const plotLinks = document.querySelectorAll('.plot-link');
const plotModal = document.getElementById('plotModal');
const plotNameInput = document.getElementById('plot-name');
const plotContentInput = document.getElementById('plot-content');
const savePlotBtn = document.getElementById('save-plot');
plotLinks.forEach(link => {
link.addEventListener('click', function (e) {
e.preventDefault();
const name = this.dataset.name;
const content = this.dataset.content;
plotNameInput.value = name;
plotContentInput.value = content;
});
});
savePlotBtn.addEventListener('click', async function () {
const name = plotNameInput.value;
const content = plotContentInput.value;
try {
const response = await fetch('/update_plot', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `name=${encodeURIComponent(name)}&content=${encodeURIComponent(content)}`
});
if (response.ok) {
// Recharger la page pour mettre à jour la liste
window.location.reload();
} else {
alert('Erreur lors de la sauvegarde de l\'intrigue');
}
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors de la sauvegarde de l\'intrigue');
}
});
// Gestion des liens vers les personnages et les intrigues
document.getElementById('preview-content').addEventListener('click', async function (e) {
const link = e.target.closest('.character-link, .plot-link');
if (link) {
e.preventDefault();
const type = link.dataset.type;
const name = link.dataset.name;
try {
const response = await fetch(`/get_${type}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `name=${encodeURIComponent(name)}`
});
if (response.ok) {
const data = await response.json();
editor.setValue(data.content);
// Mettre à jour le titre de l'éditeur
document.querySelector('.editor h2').textContent = `Éditeur de ${type === 'character' ? 'Personnage' : 'Intrigue'}: ${name}`;
updateSaveButtonText();
} else {
alert(`Erreur lors du chargement du ${type}`);
}
} catch (error) {
console.error('Erreur:', error);
alert(`Erreur lors du chargement du ${type}`);
}
}
});
// Gestion des liens dans la liste des personnages
const sidebarCharacterLinks = document.querySelectorAll('.sidebar .character-link');
sidebarCharacterLinks.forEach(link => {
link.addEventListener('click', async function (e) {
e.preventDefault();
const name = this.dataset.name;
try {
const response = await fetch('/get_character', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `name=${encodeURIComponent(name)}`
});
if (response.ok) {
const data = await response.json();
editor.setValue(data.content);
document.querySelector('.editor h2').textContent = `Éditeur de Personnage: ${name}`;
} else {
alert('Erreur lors du chargement du personnage');
}
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors du chargement du personnage');
}
});
});
// Gestion des liens dans la liste des intrigues
const sidebarPlotLinks = document.querySelectorAll('.sidebar .plot-link');
sidebarPlotLinks.forEach(link => {
link.addEventListener('click', async function (e) {
e.preventDefault();
const name = this.dataset.name;
try {
const response = await fetch('/get_plot', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `name=${encodeURIComponent(name)}`
});
if (response.ok) {
const data = await response.json();
editor.setValue(data.content);
document.querySelector('.editor h2').textContent = `Éditeur d'Intrigue: ${name}`;
} else {
alert('Erreur lors du chargement de l\'intrigue');
}
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors du chargement de l\'intrigue');
}
});
});
// Lien vers le livre
const bookLink = document.querySelector('.book-link');
if (bookLink) {
bookLink.addEventListener('click', (e) => {
e.preventDefault();
loadBook();
updateSaveButtonText();
});
}
// Lien vers le fichier des personnages
const charactersFileLink = document.querySelector('.characters-file-link');
if (charactersFileLink) {
charactersFileLink.addEventListener('click', (e) => {
e.preventDefault();
loadCharactersFile();
updateSaveButtonText();
});
}
// Lien vers le fichier des intrigues
const plotsFileLink = document.querySelector('.plots-file-link');
if (plotsFileLink) {
plotsFileLink.addEventListener('click', (e) => {
e.preventDefault();
loadPlotsFile();
updateSaveButtonText();
});
}
// Gestion des boutons de déplacement
const moveUpBtn = document.getElementById('move-up-btn');
const moveDownBtn = document.getElementById('move-down-btn');
if (moveUpBtn) {
moveUpBtn.addEventListener('click', () => moveBlock('up'));
}
if (moveDownBtn) {
moveDownBtn.addEventListener('click', () => moveBlock('down'));
}
// Gestion du focus pour les boutons de déplacement
editor.getWrapperElement().addEventListener('focus', updateMoveButtonsState);
editor.getWrapperElement().addEventListener('blur', updateMoveButtonsState);
// Gestion des raccourcis clavier
document.addEventListener('keydown', function (e) {
if (e.altKey && editor.hasFocus()) {
if (e.key === 'ArrowUp') {
e.preventDefault();
moveBlock('up');
} else if (e.key === 'ArrowDown') {
e.preventDefault();
moveBlock('down');
}
}
});
// État initial des boutons
updateMoveButtonsState();
// Adapter la taille de l'éditeur
editor.setSize(null, 'calc(100vh - 100px)');
// Gérer le changement de thème
const themeToggle = document.getElementById('theme-toggle');
themeToggle.addEventListener('click', function () {
const isDark = document.body.getAttribute('data-theme') === 'dark';
editor.setOption('theme', isDark ? 'monokai' : 'default');
});
// Gérer les changements de contenu
editor.on('change', function () {
updatePreview();
const editorTitle = document.querySelector('.editor h2').textContent;
if (editorTitle === 'Livre') {
updateWordCount();
}
updateSaveButtonText();
});
// Gérer le focus
editor.on('focus', function () {
updateMoveButtonsState();
});
editor.on('blur', function () {
updateMoveButtonsState();
});
});
// Fonction pour ajouter un bloc de commentaire
function addCommentBlock() {
const cursor = editor.getCursor();
const commentBlock = '\n#+begin_comment\n\n#+end_comment\n';
// Insérer le bloc de commentaire à la position du curseur
editor.replaceRange(commentBlock, cursor);
// Placer le curseur dans le bloc de commentaire
const newCursor = {
line: cursor.line + 2,
ch: 0
};
editor.setCursor(newCursor);
editor.focus();
}
// Fonction pour trouver les limites du bloc de texte courant
function findCurrentBlockBoundaries(content, cursorPosition) {
const lines = editor.getValue().split('\n');
let currentLine = 0;
let charCount = 0;
let startLine = 0;
let endLine = lines.length - 1;
let currentLineNumber = 0;
// Trouver la ligne courante
while (currentLine < lines.length) {
const lineLength = lines[currentLine].length + 1; // +1 pour le saut de ligne
if (charCount + lineLength > cursorPosition) {
currentLineNumber = currentLine;
break;
}
charCount += lineLength;
currentLine++;
}
// Trouver le titre précédent du même niveau ou supérieur
for (let i = currentLineNumber - 1; i >= 0; i--) {
const match = lines[i].match(/^(\*+)\s/);
if (match) {
const level = match[1].length;
if (level <= blockLevel) {
startLine = i;
break;
}
}
}
// Trouver le titre suivant du même niveau ou supérieur
for (let i = currentLineNumber + 1; i < lines.length; i++) {
const match = lines[i].match(/^(\*+)\s/);
if (match) {
const level = match[1].length;
if (level <= blockLevel) {
endLine = i - 1;
break;
}
}
}
return { startLine, endLine };
}
// Fonction pour déplacer un bloc de texte
function moveBlock(direction) {
const content = editor.getValue();
const cursorPosition = editor.getCursor().ch;
const pos = editor.posFromIndex(editor.indexFromPos(cursorPosition));
// Trouver les limites du bloc actuel
const { startLine, endLine } = findCurrentBlockBoundaries(content, pos.line);
// Calculer la nouvelle position
let newStartLine = startLine;
const blockLines = content.split('\n').slice(startLine, endLine + 1);
const blockLevel = blockLines[0].match(/^\*+/)[0].length;
if (direction === 'up') {
// Trouver le titre précédent du même niveau ou supérieur
for (let i = startLine - 1; i >= 0; i--) {
const match = content.split('\n')[i].match(/^(\*+)\s/);
if (match) {
const level = match[1].length;
if (level <= blockLevel) {
newStartLine = i;
break;
}
}
}
} else {
// Trouver le titre suivant du même niveau ou supérieur
const lines = content.split('\n');
for (let i = endLine + 1; i < lines.length; i++) {
const match = lines[i].match(/^(\*+)\s/);
if (match) {
const level = match[1].length;
if (level <= blockLevel) {
newStartLine = i;
break;
}
}
}
}
// Déplacer le bloc
const lines = content.split('\n');
const block = lines.slice(startLine, endLine + 1).join('\n');
const newLines = [...lines];
newLines.splice(startLine, endLine - startLine + 1);
newLines.splice(newStartLine, 0, block);
// Mettre à jour le contenu
editor.setValue(newLines.join('\n'));
// Calculer la nouvelle position du curseur
const relativePos = pos.line - startLine;
const newCursorPos = { line: newStartLine + relativePos, ch: pos.ch };
// Mettre à jour la prévisualisation
updatePreview();
// Restaurer la position du curseur
editor.setCursor(newCursorPos);
editor.focus();
}
// Fonction pour mettre à jour l'état des boutons de déplacement
function updateMoveButtonsState() {
const hasFocus = editor.hasFocus();
document.getElementById('move-up-btn').disabled = !hasFocus;
document.getElementById('move-down-btn').disabled = !hasFocus;
}
function initializeEditor() {
// État initial des boutons
updateMoveButtonsState();
// Adapter la taille de l'éditeur
editor.setSize(null, 'calc(100vh - 100px)');
}
// Fonction pour mettre à jour le texte du bouton de sauvegarde
function updateSaveButtonText() {
const editorTitle = document.querySelector('.editor h2').textContent;
const updateBtn = document.getElementById('update-btn');
let fileText = '';
if (editorTitle.startsWith('Éditeur de Personnage:')) {
const name = editorTitle.split(':')[1].trim();
fileText = `Sauvegarder ${name}`;
} else if (editorTitle.startsWith('Éditeur d\'Intrigue:')) {
const name = editorTitle.split(':')[1].trim();
fileText = `Sauvegarder ${name}`;
} else if (editorTitle === 'Personnages') {
fileText = 'Sauvegarder personnages.org';
} else if (editorTitle === 'Intrigues') {
fileText = 'Sauvegarder intrigues.org';
} else {
fileText = 'Sauvegarder livre.org';
}
updateBtn.textContent = fileText;
}