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 = '
$1'); // Conversion du code text = text.replace(/^#\+BEGIN_SRC.*\n(.*?)\n#\+END_SRC$/gs, '
$1
');
text = text.replace(/`([^`]+)`/g, '$1
');
// Conversion des paragraphes
text = text.split('\n\n').map(para => {
if (!para.trim()) return '';
if (!para.match(/^<[hul]|^${para}`; } 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; }