mirror of
https://forge.chapril.org/tykayn/book_generator
synced 2025-06-20 01:34:43 +02:00
1133 lines
No EOL
36 KiB
JavaScript
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;
|
|
}
|