ajout app flask pour prévisualiser le livre

This commit is contained in:
Tykayn 2025-03-04 22:36:16 +01:00 committed by tykayn
parent 375fbb3a7a
commit 9f1b265a21
29 changed files with 4533 additions and 137 deletions

393
app.py
View file

@ -1,80 +1,359 @@
from flask import Flask, render_template, request, jsonify, url_for
from flask import Flask, render_template, request, jsonify
import os
from datetime import datetime
import json
from datetime import datetime, timedelta
import re
app = Flask(__name__, static_folder='static')
def count_words_in_text(text):
# Supprime les lignes commençant par * (titres org)
text = re.sub(r'^\*+.*$', '', text, flags=re.MULTILINE)
# Supprime les lignes commençant par # (commentaires)
text = re.sub(r'^#.*$', '', text, flags=re.MULTILINE)
# Compte les mots (séquences de caractères non-espaces)
words = re.findall(r'\S+', text)
def load_org_file(filename):
if os.path.exists(filename):
with open(filename, 'r', encoding='utf-8') as f:
return f.read()
return ""
def save_org_file(filename, content):
with open(filename, 'w', encoding='utf-8') as f:
f.write(content)
def extract_characters(content):
characters = []
current_character = None
current_content = []
for line in content.split('\n'):
if line.startswith('** '):
if current_character:
characters.append({
'name': current_character,
'content': '\n'.join(current_content)
})
current_character = line[3:].strip()
current_content = []
elif current_character:
current_content.append(line)
if current_character:
characters.append({
'name': current_character,
'content': '\n'.join(current_content)
})
return characters
def extract_plots(content):
plots = []
current_plot = None
current_content = []
current_level = 0
for line in content.split('\n'):
if line.startswith('*'):
if current_plot:
current_plot['content'] = '\n'.join(current_content)
plots.append(current_plot)
level = len(line) - len(line.lstrip('*'))
current_plot = {
'name': line.lstrip('*').strip(),
'level': level,
'content': '',
'subplots': []
}
current_content = []
elif current_plot:
current_content.append(line)
if current_plot:
current_plot['content'] = '\n'.join(current_content)
plots.append(current_plot)
# Organiser les intrigues en hiérarchie
hierarchical_plots = []
plot_stack = []
for plot in plots:
while plot_stack and plot['level'] <= plot_stack[-1]['level']:
plot_stack.pop()
if plot_stack:
plot_stack[-1]['subplots'].append(plot)
else:
hierarchical_plots.append(plot)
plot_stack.append(plot)
return hierarchical_plots
def update_data_json():
data = load_data()
# Charger et traiter les personnages
characters_content = load_org_file('personnages.org')
characters = extract_characters(characters_content)
data['characters'] = characters
# Charger et traiter les intrigues
plots_content = load_org_file('intrigues.org')
plots = extract_plots(plots_content)
data['plots'] = plots
save_data(data)
return data
def count_words(text):
# Ignorer les lignes de propriétés et d'export
lines = text.split('\n')
filtered_lines = [line for line in lines if not any(line.startswith(prefix) for prefix in
[':PROPERTIES:', ':ID:', ':END:', '#+BEGIN_EXPORT', '#+END_EXPORT', '#+'])]
# Compter les mots dans les lignes filtrées
words = ' '.join(filtered_lines).split()
return len(words)
def read_org_file():
with open('livre.org', 'r', encoding='utf-8') as f:
return f.read()
def update_word_count():
try:
def load_data():
if os.path.exists('data.json'):
with open('data.json', 'r', encoding='utf-8') as f:
data = json.load(f)
except FileNotFoundError:
data = {}
today = datetime.now().strftime('%Y-%m-%d')
content = read_org_file()
current_words = count_words_in_text(content)
# Si on a déjà un compteur pour aujourd'hui, on calcule la différence
if today in data:
previous_words = data[today]
if current_words > previous_words:
data[today] = current_words - previous_words
else:
data[today] = 0
else:
data[today] = current_words
return json.load(f)
return {}
def save_data(data):
with open('data.json', 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
return data[today]
json.dump(data, f, ensure_ascii=False, indent=2)
def count_words_today():
try:
with open('data.json', 'r', encoding='utf-8') as f:
data = json.load(f)
today = datetime.now().strftime('%Y-%m-%d')
return data.get(today, 0)
except FileNotFoundError:
return update_word_count()
def get_words_today():
data = load_data()
today = datetime.now().strftime('%Y-%m-%d')
yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
# Si on n'a pas de données pour aujourd'hui, on crée une entrée
if today not in data:
data[today] = {
'words': 0,
'last_update': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
save_data(data)
# Trouver le jour précédent le plus proche qui a des données
previous_day = None
previous_words = 0
current_date = datetime.now() - timedelta(days=1)
while current_date >= datetime.strptime('2000-01-01', '%Y-%m-%d'):
date_str = current_date.strftime('%Y-%m-%d')
if date_str in data:
previous_day = date_str
previous_words = data[date_str]['words']
break
current_date -= timedelta(days=1)
# Si on n'a pas trouvé de jour précédent, on utilise le nombre de mots actuel comme référence
if not previous_day:
with open('livre.org', 'r', encoding='utf-8') as f:
content = f.read()
previous_words = count_words(content)
data[yesterday] = {
'words': previous_words,
'last_update': (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')
}
save_data(data)
# Calculer la progression en différence avec le jour précédent
words_today = data[today]['words']
progress = max(0, words_today - previous_words)
return progress
@app.route('/')
def index():
content = read_org_file()
words_today = count_words_today()
return render_template('index.html', content=content, words_today=words_today)
with open('livre.org', 'r', encoding='utf-8') as f:
content = f.read()
words_today = get_words_today()
data = update_data_json()
return render_template('index.html', content=content, words_today=words_today,
characters=data.get('characters', []), plots=data.get('plots', []))
@app.route('/update', methods=['POST'])
def update():
new_content = request.form.get('content')
with open('livre.org', 'w', encoding='utf-8') as f:
f.write(new_content)
content = request.form.get('content', '')
editor_title = request.form.get('editor_title', '')
# Met à jour le compteur de mots
words_today = update_word_count()
return jsonify({'status': 'success', 'words_today': words_today})
# Déterminer le fichier de destination en fonction du titre de l'éditeur
if 'Personnage:' in editor_title:
current_file = 'personnages.org'
elif 'Intrigue:' in editor_title:
current_file = 'intrigues.org'
elif editor_title == 'Intrigues':
current_file = 'intrigues.org'
else:
current_file = 'livre.org'
with open(current_file, 'w', encoding='utf-8') as f:
f.write(content)
# Mettre à jour le nombre de mots pour aujourd'hui si c'est le fichier principal
if current_file == 'livre.org':
data = load_data()
today = datetime.now().strftime('%Y-%m-%d')
data[today] = {
'words': count_words(content),
'last_update': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
save_data(data)
return jsonify({'words_today': get_words_today()})
@app.route('/words_today')
def get_words_today():
return jsonify({'words': count_words_today()})
def words_today():
return jsonify({'words': get_words_today()})
@app.route('/progress_data')
def progress_data():
data = load_data()
# Trier les dates
dates = sorted(data.keys(), reverse=True)
# Prendre les 7 derniers jours
dates = dates[:7]
# Préparer les données pour le graphique
chart_data = {
'labels': [],
'words': [],
'progress': []
}
for date in dates:
chart_data['labels'].append(date)
if date in data and 'words' in data[date]:
chart_data['words'].append(data[date]['words'])
else:
chart_data['words'].append(0)
# Calculer la progression entre chaque jour
for i in range(len(chart_data['words'])):
if i < len(chart_data['words']) - 1:
progress = max(0, chart_data['words'][i] - chart_data['words'][i + 1])
else:
progress = 0
chart_data['progress'].append(progress)
return jsonify(chart_data)
@app.route('/update_character', methods=['POST'])
def update_character():
name = request.form.get('name')
content = request.form.get('content')
characters_content = load_org_file('personnages.org')
characters = extract_characters(characters_content)
# Mettre à jour ou ajouter le personnage
found = False
for char in characters:
if char['name'] == name:
char['content'] = content
found = True
break
if not found:
characters.append({
'name': name,
'content': content
})
# Sauvegarder le fichier
new_content = []
for char in characters:
new_content.append(f"* {char['name']}")
new_content.append(char['content'])
save_org_file('personnages.org', '\n'.join(new_content))
update_data_json()
return jsonify({'status': 'success'})
@app.route('/update_plot', methods=['POST'])
def update_plot():
name = request.form.get('name')
content = request.form.get('content')
plots_content = load_org_file('intrigues.org')
plots = extract_plots(plots_content)
# Mettre à jour ou ajouter l'intrigue
found = False
for plot in plots:
if plot['name'] == name:
plot['content'] = content
found = True
break
if not found:
plots.append({
'name': name,
'content': content
})
# Sauvegarder le fichier
new_content = []
for plot in plots:
new_content.append(f"* {plot['name']}")
new_content.append(plot['content'])
save_org_file('intrigues.org', '\n'.join(new_content))
update_data_json()
return jsonify({'status': 'success'})
@app.route('/get_character', methods=['POST'])
def get_character():
name = request.form.get('name')
characters_content = load_org_file('personnages.org')
characters = extract_characters(characters_content)
for char in characters:
if char['name'] == name:
return jsonify({
'content': f"* {char['name']}\n{char['content']}"
})
return jsonify({'error': 'Personnage non trouvé'}), 404
@app.route('/get_plot', methods=['POST'])
def get_plot():
name = request.form.get('name')
plots_content = load_org_file('intrigues.org')
plots = extract_plots(plots_content)
for plot in plots:
if plot['name'] == name:
return jsonify({
'content': f"* {plot['name']}\n{plot['content']}"
})
return jsonify({'error': 'Intrigue non trouvée'}), 404
@app.route('/get_book')
def get_book():
content = load_org_file('livre.org')
return jsonify({'content': content})
@app.route('/get_plots_file')
def get_plots_file():
try:
with open('intrigues.org', 'r', encoding='utf-8') as f:
content = f.read()
return jsonify({'content': content})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/get_characters_file')
def get_characters_file():
try:
with open('personnages.org', 'r', encoding='utf-8') as f:
content = f.read()
return jsonify({'content': content})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
# Initialise le compteur de mots au démarrage si nécessaire
count_words_today()
app.run(debug=True)

View file

@ -1,3 +1,71 @@
{
"2025-03-04": 8
"2025-03-04": {
"words": 229,
"last_update": "2025-03-04 22:36:06"
},
"2025-03-03": {
"words": 200,
"last_update": "2025-03-03 16:14:22"
},
"characters": [],
"plots": [
{
"name": "intrigue 1 2-3",
"level": 1,
"content": "",
"subplots": [
{
"name": "ce matin un lapin 2-3",
"level": 2,
"content": "",
"subplots": []
}
]
},
{
"name": "intrigue 2 4-5",
"level": 1,
"content": "",
"subplots": [
{
"name": "sous partie 1 de 2 3-8",
"level": 2,
"content": "",
"subplots": []
},
{
"name": "sous partie 2 de 2 5-9",
"level": 2,
"content": "",
"subplots": []
}
]
},
{
"name": "intrigue 3 4-6",
"level": 1,
"content": "",
"subplots": [
{
"name": "sous partie 3.1",
"level": 2,
"content": "",
"subplots": []
}
]
},
{
"name": "tout 1-10",
"level": 1,
"content": "",
"subplots": [
{
"name": "épilogue 9-10",
"level": 2,
"content": "",
"subplots": []
}
]
}
]
}

View file

@ -1,39 +1,37 @@
:PROPERTIES:
:ID: 1b3c6217-f565-42d9-b16b-db11644f6121
:END:
#+title: livre example_livre
#+AUTHOR: (votre nom)
#+EMAIL: votre@email.com
#+BEGIN_EXPORT epub
:title "Mon livre"
:author "Votre nom"
:email "votre@email.com"
:language "fr"
:encoding "UTF-8"
:subject "Littérature"
:description "Ceci est un livre écrit en Org-mode"
:keywords "Org-mode, livre, électronique"
:cover "image/cover.jpg"
#+END_EXPORT
#+title: livre example_livre
#+AUTHOR: (votre nom)
#+EMAIL: votre@email.com
#+BEGIN_EXPORT epub
:title "Mon livre"
:author "Votre nom"
:email "votre@email.com"
:language "fr"
:encoding "UTF-8"
:subject "Littérature"
:description "Ceci est un livre écrit en Org-mode"
:keywords "Org-mode, livre, électronique"
:cover "image/cover.jpg"
#+END_EXPORT
* Livre nom_de_mon_livre :title:
eeeeeeeee préambule du cul
dfgdgg dsg dsgd gbfgfgghfhghg dsg dsgd gbfgfgghfhghg dsg dsgd gbfgfgghfhghg dsg dsgd gbfgfgghfhghg
et il était un gens qui faisait nimp
** préambule du cul
eeeeeeeeeeeeeeeeeeeee préambule du cul eeeeeeeeeeeeeeeeee
ne devrait pas avoir de titre
eeeeeeeeeeeeeeeeeeeee préambule du cul eeeeeeeeeeeeeeeeee
cette partie ne devrait pas avoir de titre
allez hein zou zou
** Chapitre 0
--------------
--------------
là non plus pas de titre à afficher
-------------
-------------
** Chapitre 1 :title:
celui là on doit le voir: chapitre 1 au dessus ici.
Dans un monde lointain, il y avait une île mystérieuse où les arbres avaient des feuilles qui brillaient comme des étoiles. Un jeune aventurier nommé Eryndor y arriva un jour, attiré par les légendes de l'île. Il découvrit un temple caché où les dieux anciens avaient laissé des secrets et des pouvoirs magiques.
celui là on doit le voir: chapitre 1 au dessus ici.
Dans un monde lointain, il y avait une île mystérieuse où les arbres avaient des feuilles qui brillaient comme des étoiles. Un jeune aventurier nommé Eryndor y arriva un jour, attiré par les légendes de l'île. Il découvrit un temple caché où les dieux anciens avaient laissé des secrets et des pouvoirs magiques.
*** scène d'exposition
#+begin_comment
@ -41,23 +39,21 @@ là non plus pas de titre à afficher
On devrait mettre un peu plus d'électro swing dans cette partie.
Ce commentaire n'appraîtra pas à l'export. C'est une notre spécialement pour l'auteur.
#+end_comment
blah blah
bleh
bob trouva un cristal qui lui permit de communiquer avec les esprits de la nature. Avec leur aide, il put vaincre les ténèbres qui menaçaient l'île et restaurer la lumière éternelle. L'île fut sauvée et Eryndor devint un héros légendaire.
blah blah
bleh bob trouva un cristal qui lui permit de communiquer avec les esprits de la nature. Avec leur aide, il put vaincre les ténèbres qui menaçaient l'île et restaurer la lumière éternelle. L'île fut sauvée et Eryndor devint un héros légendaire.
1111111111111111
** Chapitre 2 :title:
2222222222222
#+begin_comment
ouaish heuuuu
2222222222222
chuck fait des trucs
#+begin_comment
commentaire làààà
oui bon heu
#+end_comment
#+end_comment
chuck fait des trucs
** Chapitre 3 :title:
33333333333333333
bobette et bob sont sur un bateau

22
personnages.org Normal file
View file

@ -0,0 +1,22 @@
* bob
* bob
- nom:
- personnalité:
- objectifs:
- conflits:
- évolution:
- alias: Bob l'éponge, SpongeBob
* chuck norris
- nom:
- personnalité:
- objectifs:
- conflits:
- évolution:
- alias: le roux; celui dont on ne doit pas prononcer le nom
* bobette
- nom:
- personnalité:
- objectifs:
- conflits:
- évolution:
- alias:

2078
static/css/lib/bootstrap-icons.min.css vendored Normal file

File diff suppressed because it is too large Load diff

6
static/css/lib/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
static/css/lib/codemirror.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
static/css/lib/eclipse.min.css vendored Normal file
View file

@ -0,0 +1 @@
.cm-s-eclipse span.cm-meta{color:#ff1717}.cm-s-eclipse span.cm-keyword{line-height:1em;font-weight:700;color:#7f0055}.cm-s-eclipse span.cm-atom{color:#219}.cm-s-eclipse span.cm-number{color:#164}.cm-s-eclipse span.cm-def{color:#00f}.cm-s-eclipse span.cm-variable{color:#000}.cm-s-eclipse span.cm-variable-2{color:#0000c0}.cm-s-eclipse span.cm-type,.cm-s-eclipse span.cm-variable-3{color:#0000c0}.cm-s-eclipse span.cm-property{color:#000}.cm-s-eclipse span.cm-operator{color:#000}.cm-s-eclipse span.cm-comment{color:#3f7f5f}.cm-s-eclipse span.cm-string{color:#2a00ff}.cm-s-eclipse span.cm-string-2{color:#f50}.cm-s-eclipse span.cm-qualifier{color:#555}.cm-s-eclipse span.cm-builtin{color:#30a}.cm-s-eclipse span.cm-bracket{color:#cc7}.cm-s-eclipse span.cm-tag{color:#170}.cm-s-eclipse span.cm-attribute{color:#00c}.cm-s-eclipse span.cm-link{color:#219}.cm-s-eclipse span.cm-error{color:red}.cm-s-eclipse .CodeMirror-activeline-background{background:#e8f2ff}.cm-s-eclipse .CodeMirror-matchingbracket{outline:1px solid grey;color:#000!important}

6
static/css/lib/fontawesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

1
static/css/lib/monokai.min.css vendored Normal file
View file

@ -0,0 +1 @@
.cm-s-monokai.CodeMirror{background:#272822;color:#f8f8f2}.cm-s-monokai div.CodeMirror-selected{background:#49483e}.cm-s-monokai .CodeMirror-line::selection,.cm-s-monokai .CodeMirror-line>span::selection,.cm-s-monokai .CodeMirror-line>span>span::selection{background:rgba(73,72,62,.99)}.cm-s-monokai .CodeMirror-line::-moz-selection,.cm-s-monokai .CodeMirror-line>span::-moz-selection,.cm-s-monokai .CodeMirror-line>span>span::-moz-selection{background:rgba(73,72,62,.99)}.cm-s-monokai .CodeMirror-gutters{background:#272822;border-right:0}.cm-s-monokai .CodeMirror-guttermarker{color:#fff}.cm-s-monokai .CodeMirror-guttermarker-subtle{color:#d0d0d0}.cm-s-monokai .CodeMirror-linenumber{color:#d0d0d0}.cm-s-monokai .CodeMirror-cursor{border-left:1px solid #f8f8f0}.cm-s-monokai span.cm-comment{color:#75715e}.cm-s-monokai span.cm-atom{color:#ae81ff}.cm-s-monokai span.cm-number{color:#ae81ff}.cm-s-monokai span.cm-comment.cm-attribute{color:#97b757}.cm-s-monokai span.cm-comment.cm-def{color:#bc9262}.cm-s-monokai span.cm-comment.cm-tag{color:#bc6283}.cm-s-monokai span.cm-comment.cm-type{color:#5998a6}.cm-s-monokai span.cm-attribute,.cm-s-monokai span.cm-property{color:#a6e22e}.cm-s-monokai span.cm-keyword{color:#f92672}.cm-s-monokai span.cm-builtin{color:#66d9ef}.cm-s-monokai span.cm-string{color:#e6db74}.cm-s-monokai span.cm-variable{color:#f8f8f2}.cm-s-monokai span.cm-variable-2{color:#9effff}.cm-s-monokai span.cm-type,.cm-s-monokai span.cm-variable-3{color:#66d9ef}.cm-s-monokai span.cm-def{color:#fd971f}.cm-s-monokai span.cm-bracket{color:#f8f8f2}.cm-s-monokai span.cm-tag{color:#f92672}.cm-s-monokai span.cm-header{color:#ae81ff}.cm-s-monokai span.cm-link{color:#ae81ff}.cm-s-monokai span.cm-error{background:#f92672;color:#f8f8f0}.cm-s-monokai .CodeMirror-activeline-background{background:#373831}.cm-s-monokai .CodeMirror-matchingbracket{text-decoration:underline;color:#fff!important}

View file

@ -7,6 +7,14 @@
--preview-bg: #ffffff;
--code-bg: #f8f9fa;
--blockquote-color: #6c757d;
--link-color: #0d6efd;
--link-hover-color: #0a58ca;
--progress-bg: #e9ecef;
--progress-bar-bg: #0d6efd;
--title-bg: #e3f2fd;
--title-color: #1976d2;
--comment-bg: #f5f5f5;
--comment-color: #757575;
}
[data-theme="dark"] {
@ -18,6 +26,14 @@
--preview-bg: #2b3035;
--code-bg: #343a40;
--blockquote-color: #adb5bd;
--link-color: #6ea8fe;
--link-hover-color: #8bb9fe;
--progress-bg: #495057;
--progress-bar-bg: #6ea8fe;
--title-bg: #1a237e;
--title-color: #90caf9;
--comment-bg: #424242;
--comment-color: #bdbdbd;
}
body {
@ -30,6 +46,7 @@ body {
background-color: var(--sidebar-bg);
padding: 20px;
border-right: 1px solid var(--border-color);
overflow-y: auto;
}
.editor {
@ -49,6 +66,142 @@ body {
color: var(--text-color);
}
/* Styles pour CodeMirror */
.CodeMirror {
height: calc(100vh - 100px);
font-family: monospace;
font-size: 14px;
line-height: 1.6;
border: 1px solid var(--border-color);
border-radius: 4px;
}
.CodeMirror-scroll {
height: 100%;
}
.CodeMirror-gutters {
background-color: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
}
.CodeMirror-linenumber {
color: var(--text-color);
opacity: 0.7;
}
.CodeMirror-cursor {
border-left: 2px solid var(--text-color);
}
/* Thème clair */
[data-theme="light"] .CodeMirror {
background-color: var(--editor-bg);
color: var(--text-color);
}
[data-theme="light"] .cm-header-1 {
color: #1976d2;
}
[data-theme="light"] .cm-header-2 {
color: #2196f3;
}
[data-theme="light"] .cm-header-3 {
color: #42a5f5;
}
[data-theme="light"] .cm-header-4 {
color: #64b5f6;
}
[data-theme="light"] .cm-header-5 {
color: #90caf9;
}
[data-theme="light"] .cm-header-6 {
color: #bbdefb;
}
[data-theme="light"] .cm-comment {
color: #757575;
}
[data-theme="light"] .cm-keyword {
color: #7b1fa2;
}
[data-theme="light"] .cm-string {
color: #2e7d32;
}
[data-theme="light"] .cm-property {
color: #1976d2;
}
[data-theme="light"] .cm-variable {
color: #1976d2;
}
[data-theme="light"] .cm-number {
color: #f57c00;
}
/* Thème sombre */
[data-theme="dark"] .CodeMirror {
background-color: var(--editor-bg);
color: var(--text-color);
}
[data-theme="dark"] .cm-header-1 {
color: #90caf9;
}
[data-theme="dark"] .cm-header-2 {
color: #64b5f6;
}
[data-theme="dark"] .cm-header-3 {
color: #42a5f5;
}
[data-theme="dark"] .cm-header-4 {
color: #2196f3;
}
[data-theme="dark"] .cm-header-5 {
color: #1976d2;
}
[data-theme="dark"] .cm-header-6 {
color: #1565c0;
}
[data-theme="dark"] .cm-comment {
color: #bdbdbd;
}
[data-theme="dark"] .cm-keyword {
color: #ce93d8;
}
[data-theme="dark"] .cm-string {
color: #81c784;
}
[data-theme="dark"] .cm-property {
color: #90caf9;
}
[data-theme="dark"] .cm-variable {
color: #90caf9;
}
[data-theme="dark"] .cm-number {
color: #ffb74d;
}
.word-count {
background-color: var(--code-bg);
padding: 15px;
@ -56,6 +209,37 @@ body {
margin-bottom: 20px;
}
.word-count .progress {
height: 8px;
background-color: var(--progress-bg);
border-radius: 4px;
overflow: hidden;
}
.word-count .progress-bar {
background-color: var(--progress-bar-bg);
transition: width 0.3s ease;
}
.word-count input[type="number"] {
background-color: var(--editor-bg);
border-color: var(--border-color);
color: var(--text-color);
}
.word-count input[type="number"]:focus {
background-color: var(--editor-bg);
border-color: var(--link-color);
color: var(--text-color);
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.word-count .input-group-text {
background-color: var(--code-bg);
border-color: var(--border-color);
color: var(--text-color);
}
.preview-panel {
height: 100vh;
padding: 20px;
@ -124,9 +308,361 @@ body {
margin: 1em 0;
}
.theme-switch {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
/* Styles pour la table des matières */
.table-of-contents {
margin-bottom: 20px;
padding: 15px;
background-color: var(--code-bg);
border-radius: 4px;
}
.table-of-contents h5 {
margin-bottom: 10px;
color: var(--text-color);
}
.toc-list {
list-style: none;
padding: 0;
margin: 0;
}
.toc-item {
margin: 5px 0;
}
.toc-item a {
color: var(--link-color);
text-decoration: none;
display: block;
padding: 2px 0;
transition: color 0.2s;
}
.toc-item a:hover {
color: var(--link-hover-color);
}
.level-1 {
font-weight: bold;
margin-left: 0;
}
.level-2 {
margin-left: 15px;
}
.level-3 {
margin-left: 30px;
font-size: 0.9em;
}
.level-4 {
margin-left: 45px;
font-size: 0.85em;
}
.level-5 {
margin-left: 60px;
font-size: 0.8em;
}
/* Styles pour les titres et commentaires */
.preview-content .title-section {
background-color: var(--title-bg);
color: var(--title-color);
padding: 10px 15px;
margin: 10px 0;
border-radius: 4px;
border-left: 4px solid var(--title-color);
}
.preview-content .comment-section {
background-color: var(--comment-bg);
color: var(--comment-color);
padding: 10px 15px;
margin: 10px 0;
border-radius: 4px;
border-left: 4px solid var(--comment-color);
font-style: italic;
}
.preview-content .hidden {
display: none;
}
/* Styles pour les filtres */
.filters {
background-color: var(--code-bg);
padding: 15px;
border-radius: 4px;
}
.filters h5 {
margin-bottom: 10px;
color: var(--text-color);
}
.filters .form-check-label {
color: var(--text-color);
}
.filters .form-check {
position: relative;
display: flex;
align-items: center;
gap: 5px;
}
.info-icon {
color: var(--text-color);
opacity: 0.7;
cursor: help;
font-size: 0.9em;
transition: opacity 0.2s;
}
.info-icon:hover,
.info-icon:focus {
opacity: 1;
}
/* Styles pour les tooltips */
.tooltip {
font-size: 0.875rem;
}
.tooltip-inner {
max-width: 300px;
padding: 0.5rem 1rem;
background-color: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--border-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tooltip.bs-tooltip-end .tooltip-arrow::before {
border-right-color: var(--border-color);
}
.tooltip.bs-tooltip-end .tooltip-arrow::after {
border-right-color: var(--bg-color);
}
.progress-chart {
background-color: var(--code-bg);
padding: 15px;
border-radius: 4px;
height: 200px;
}
.progress-chart h5 {
margin-bottom: 10px;
color: var(--text-color);
}
/* Styles pour le graphique en mode sombre */
[data-theme="dark"] .progress-chart canvas {
filter: brightness(0.8) contrast(1.2);
}
/* Styles pour les sections de personnages et d'intrigues */
.characters-section,
.plots-section {
background-color: var(--code-bg);
padding: 15px;
border-radius: 4px;
}
.characters-section h5,
.plots-section h5 {
margin-bottom: 10px;
color: var(--text-color);
}
.list-group-item {
background-color: var(--editor-bg);
border-color: var(--border-color);
color: var(--text-color);
transition: all 0.2s;
}
.list-group-item:hover {
background-color: var(--sidebar-bg);
border-color: var(--link-color);
color: var(--link-color);
}
/* Styles pour les modales */
.modal-content {
background-color: var(--bg-color);
color: var(--text-color);
}
.modal-header {
border-bottom-color: var(--border-color);
}
.modal-footer {
border-top-color: var(--border-color);
}
.modal .form-control {
background-color: var(--editor-bg);
border-color: var(--border-color);
color: var(--text-color);
}
.modal .form-control:focus {
background-color: var(--editor-bg);
border-color: var(--link-color);
color: var(--text-color);
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.modal .btn-close {
filter: var(--btn-close-filter);
}
.characters-toc {
background-color: var(--bg-secondary);
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.characters-toc h6 {
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.word-count-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
background-color: var(--bg-secondary);
font-size: 0.8em;
}
.plots-toc {
background-color: var(--bg-secondary);
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.plots-toc h6 {
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.subplot-count-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
background-color: var(--bg-secondary);
font-size: 0.8em;
color: var(--text-secondary);
}
.toc-list {
list-style: none;
padding-left: 1rem;
}
.toc-item {
margin: 0.5rem 0;
}
.toc-item a {
color: var(--text-primary);
text-decoration: none;
display: flex;
align-items: center;
justify-content: space-between;
}
.toc-item a:hover {
color: var(--primary);
}
.level-1 {
margin-left: 0;
}
.level-2 {
margin-left: 1rem;
}
.level-3 {
margin-left: 2rem;
}
.level-4 {
margin-left: 3rem;
}
.level-5 {
margin-left: 4rem;
}
.regular-title {
color: #ccc;
}
.plots-file-link {
float: right;
font-size: 0.8em;
color: var(--text-secondary);
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.plots-file-link:hover {
color: var(--primary);
background-color: var(--bg-secondary);
}
.plots-section h5 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.comment-section {
margin-bottom: 1rem;
}
#add-comment-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem;
transition: all 0.2s ease;
}
#add-comment-btn:hover {
background-color: var(--bg-secondary);
color: var(--primary);
}
#add-comment-btn i {
font-size: 1.1em;
}
/* Styles pour les boutons désactivés */
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.btn:disabled:hover {
background-color: var(--bg-secondary);
color: var(--text-color);
}

Binary file not shown.

Binary file not shown.

7
static/js/lib/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
static/js/lib/chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
static/js/lib/codemirror.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
static/js/lib/commonlisp.min.js vendored Normal file
View file

@ -0,0 +1 @@
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(t){"use strict";t.defineMode("commonlisp",function(r){var o,i=/^(block|let*|return-from|catch|load-time-value|setq|eval-when|locally|symbol-macrolet|flet|macrolet|tagbody|function|multiple-value-call|the|go|multiple-value-prog1|throw|if|progn|unwind-protect|labels|progv|let|quote)$/,c=/^with|^def|^do|^prog|case$|^cond$|bind$|when$|unless$/,l=/^(?:[+\-]?(?:\d+|\d*\.\d+)(?:[efd][+\-]?\d+)?|[+\-]?\d+(?:\/[+\-]?\d+)?|#b[+\-]?[01]+|#o[+\-]?[0-7]+|#x[+\-]?[\da-f]+)/,n=/[^\s'`,@()\[\]";]/;function u(t){for(var e;e=t.next();)if("\\"==e)t.next();else if(!n.test(e)){t.backUp(1);break}return t.current()}function a(t,e){if(t.eatSpace())return o="ws",null;if(t.match(l))return"number";var n=t.next();if('"'==(n="\\"==n?t.next():n))return(e.tokenize=s)(t,e);if("("==n)return o="open","bracket";if(")"==n||"]"==n)return o="close","bracket";if(";"==n)return t.skipToEnd(),o="ws","comment";if(/['`,@]/.test(n))return null;if("|"==n)return t.skipTo("|")?(t.next(),"symbol"):(t.skipToEnd(),"error");if("#"==n)return"("==(n=t.next())?(o="open","bracket"):/[+\-=\.']/.test(n)||/\d/.test(n)&&t.match(/^\d*#/)?null:"|"==n?(e.tokenize=d)(t,e):":"==n?(u(t),"meta"):"\\"==n?(t.next(),u(t),"string-2"):"error";t=u(t);return"."==t?null:(o="symbol","nil"==t||"t"==t||":"==t.charAt(0)?"atom":"open"==e.lastType&&(i.test(t)||c.test(t))?"keyword":"&"==t.charAt(0)?"variable-2":"variable")}function s(t,e){for(var n,r=!1;n=t.next();){if('"'==n&&!r){e.tokenize=a;break}r=!r&&"\\"==n}return"string"}function d(t,e){for(var n,r;n=t.next();){if("#"==n&&"|"==r){e.tokenize=a;break}r=n}return o="ws","comment"}return{startState:function(){return{ctx:{prev:null,start:0,indentTo:0},lastType:null,tokenize:a}},token:function(t,e){t.sol()&&"number"!=typeof e.ctx.indentTo&&(e.ctx.indentTo=e.ctx.start+1),o=null;var n=e.tokenize(t,e);return"ws"!=o&&(null==e.ctx.indentTo?"symbol"==o&&c.test(t.current())?e.ctx.indentTo=e.ctx.start+r.indentUnit:e.ctx.indentTo="next":"next"==e.ctx.indentTo&&(e.ctx.indentTo=t.column()),e.lastType=o),"open"==o?e.ctx={prev:e.ctx,start:t.column(),indentTo:null}:"close"==o&&(e.ctx=e.ctx.prev||e.ctx),n},indent:function(t,e){var n=t.ctx.indentTo;return"number"==typeof n?n:t.ctx.start+1},closeBrackets:{pairs:'()[]{}""'},lineComment:";;",fold:"brace-paren",blockCommentStart:"#|",blockCommentEnd:"|#"}}),t.defineMIME("text/x-common-lisp","commonlisp")});

1
static/js/lib/css.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
static/js/lib/javascript.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
static/js/lib/markdown.min.js vendored Normal file

File diff suppressed because one or more lines are too long

83
static/js/lib/org.min.js vendored Normal file
View file

@ -0,0 +1,83 @@
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
CodeMirror.defineMode("org", function () {
return {
token: function (stream, state) {
// Titres
if (stream.match(/^\*+\s/)) {
stream.skipToEnd();
return "header";
}
// Commentaires
if (stream.match(/^#\s/)) {
stream.skipToEnd();
return "comment";
}
// Blocs de commentaires
if (stream.match(/^#\+BEGIN_COMMENT/)) {
state.inComment = true;
return "comment";
}
if (state.inComment && stream.match(/^#\+END_COMMENT/)) {
state.inComment = false;
return "comment";
}
if (state.inComment) {
stream.skipToEnd();
return "comment";
}
// Blocs de code
if (stream.match(/^#\+BEGIN_SRC/)) {
state.inCode = true;
return "comment";
}
if (state.inCode && stream.match(/^#\+END_SRC/)) {
state.inCode = false;
return "comment";
}
if (state.inCode) {
stream.skipToEnd();
return "string";
}
// Citations
if (stream.match(/^#\+BEGIN_QUOTE/)) {
state.inQuote = true;
return "comment";
}
if (state.inQuote && stream.match(/^#\+END_QUOTE/)) {
state.inQuote = false;
return "comment";
}
if (state.inQuote) {
stream.skipToEnd();
return "string";
}
// Liens
if (stream.match(/\[\[(.*?)\]\]/)) {
return "link";
}
// Texte normal
stream.next();
return null;
},
startState: function () {
return {
inComment: false,
inCode: false,
inQuote: false
};
}
};
});

1
static/js/lib/scheme.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
static/js/lib/xml.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

138
static/js/mode/org.js Normal file
View file

@ -0,0 +1,138 @@
(function (mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function (CodeMirror) {
"use strict";
CodeMirror.defineMode("org", function () {
return {
startState: function () {
return {
inComment: false,
inBlock: false,
blockType: null
};
},
token: function (stream, state) {
// Gestion des commentaires en ligne
if (stream.match(/^# /)) {
stream.skipToEnd();
return "comment";
}
// Gestion des lignes de paramètres
if (stream.match(/^:PROPERTIES:|^:END:|^:ID:|^#\+[A-Z_]+:/)) {
stream.skipToEnd();
return "comment";
}
// Gestion des paramètres d'export
if (stream.match(/^:title|^:author|^:email|^:language|^:encoding|^:subject|^:description|^:keywords|^:cover/)) {
stream.skipToEnd();
return "comment";
}
// Gestion des blocs de commentaires
if (stream.match(/^#\+begin_comment/)) {
state.inComment = true;
return "comment";
}
if (state.inComment && stream.match(/^#\+end_comment/)) {
state.inComment = false;
return "comment";
}
if (state.inComment) {
stream.skipToEnd();
return "comment";
}
// Gestion des blocs de code
if (stream.match(/^#\+BEGIN_SRC/)) {
state.inBlock = true;
state.blockType = "src";
return "keyword";
}
if (stream.match(/^#\+BEGIN_QUOTE/)) {
state.inBlock = true;
state.blockType = "quote";
return "keyword";
}
if (state.inBlock && stream.match(/^#\+END_(SRC|QUOTE)/)) {
state.inBlock = false;
state.blockType = null;
return "keyword";
}
if (state.inBlock) {
stream.skipToEnd();
return state.blockType === "src" ? "string" : "quote";
}
// Gestion des titres
if (stream.match(/^\*+\s/)) {
const level = stream.current().length - 1;
stream.skipToEnd();
return `header-${level}`;
}
// Gestion des listes
if (stream.match(/^-\s/)) {
stream.skipToEnd();
return "list";
}
// Gestion des liens
if (stream.match(/\[\[(.*?)\]\]/)) {
return "link";
}
// Gestion des propriétés
if (stream.match(/^:PROPERTIES:/)) {
stream.skipToEnd();
return "property";
}
if (stream.match(/^:END:/)) {
stream.skipToEnd();
return "property";
}
// Gestion des tags
if (stream.match(/:[a-zA-Z0-9_@#%:]+:/)) {
return "tag";
}
// Gestion des dates
if (stream.match(/<\d{4}-\d{2}-\d{2}(?: \w+)?>/)) {
return "date";
}
// Gestion des mots-clés
if (stream.match(/^#\+[A-Z_]+:/)) {
stream.skipToEnd();
return "keyword";
}
// Gestion des nombres
if (stream.match(/\b\d+\b/)) {
return "number";
}
// Texte normal
stream.next();
return null;
}
};
});
CodeMirror.defineMIME("text/x-org", "org");
});

View file

@ -5,26 +5,127 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Éditeur de Livre</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/lib/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/lib/bootstrap-icons.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/lib/fontawesome.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/lib/codemirror.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/lib/monokai.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/lib/eclipse.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
</head>
<body>
<div class="theme-switch">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="theme-switch">
<label class="form-check-label" for="theme-switch">Thème sombre</label>
</div>
</div>
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<div class="col-md-2 sidebar">
<h4>Statistiques</h4>
<!-- Éléments cachés pour stocker les données -->
<div id="characters-data" style="display: none;"></div>
<div id="plots-data" style="display: none;"></div>
<div class="word-count">
<h5>Mots aujourd'hui</h5>
<p id="words-today" class="h3">{{ words_today }}</p>
<h5>Compteur de mots</h5>
<p>Aujourd'hui : <span id="words-today">0</span> mots</p>
<div class="progress">
<div id="progress-bar" class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<p id="progress-text">0% de l'objectif</p>
<div class="input-group mb-3">
<span class="input-group-text">Objectif</span>
<input type="number" class="form-control" id="word-goal" value="400">
</div>
<div class="progress-chart-container" style="height: 200px;">
<canvas id="progressChart"></canvas>
</div>
</div>
<div class="comment-section mb-3">
<button id="add-comment-btn" class="btn btn-outline-primary w-100 mb-2">
<i class="bi bi-chat-square-text"></i> Ajouter un commentaire
</button>
<div class="d-flex gap-2">
<button id="move-up-btn" class="btn btn-outline-secondary flex-grow-1">
<i class="bi bi-arrow-up"></i> Déplacer vers le haut
</button>
<button id="move-down-btn" class="btn btn-outline-secondary flex-grow-1">
<i class="bi bi-arrow-down"></i> Déplacer vers le bas
</button>
</div>
</div>
<div class="navigation-section">
<h5>Navigation</h5>
<ul class="list-group">
<li class="list-group-item">
<a href="#" class="book-link" data-type="book">Livre</a>
</li>
<li class="list-group-item">
<a href="#" class="characters-file-link" data-type="characters">Personnages</a>
</li>
<li class="list-group-item">
<a href="#" class="plots-file-link" data-type="plots">Intrigues</a>
</li>
</ul>
</div>
<div class="characters-section">
<h5>Personnages</h5>
<div class="characters-toc"></div>
<ul class="list-group">
{% for character in characters %}
<li class="list-group-item">
<a href="#" class="character-link" data-type="character" data-name="{{ character.name }}">{{
character.name }}</a>
</li>
{% endfor %}
</ul>
</div>
<div class="plots-section">
<h5>Intrigues</h5>
<div class="plots-toc"></div>
</div>
<div class="table-of-contents">
<h5>Table des matières</h5>
<ul class="toc-list"></ul>
</div>
<div class="filters mb-3">
<h5>Options</h5>
<div class="theme-switch">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="theme-switch">
<label class="form-check-label" for="theme-switch">Thème sombre</label>
</div>
</div>
<h6>Prévisualisation</h6>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="auto-preview" checked>
<label class="form-check-label" for="auto-preview">Mise à jour automatique</label>
</div>
<h5>Filtres</h5>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="show-titles" checked>
<label class="form-check-label" for="show-titles">Montrer les titres avec le tag :titre:</label>
<i class="bi bi-info-circle info-icon" data-bs-toggle="tooltip" data-bs-placement="right"
title="Affiche ou masque les titres qui contiennent :titre: ou :title: dans leur contenu"></i>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="show-regular-titles">
<label class="form-check-label" for="show-regular-titles">Montrer les titres d'intrigue</label>
<i class="bi bi-info-circle info-icon" data-bs-toggle="tooltip" data-bs-placement="right"
title="Affiche ou masque les titres qui ne contiennent pas :titre: ou :title: dans leur contenu"></i>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="show-comments">
<label class="form-check-label" for="show-comments">Commentaires</label>
<i class="bi bi-info-circle info-icon" data-bs-toggle="tooltip" data-bs-placement="right"
title="Affiche ou masque les commentaires (lignes commençant par # et blocs entre #+BEGIN_COMMENT et #+END_COMMENT)"></i>
</div>
</div>
</div>
@ -39,19 +140,97 @@
<!-- Preview panel -->
<div class="col-md-5 preview-panel">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>Prévisualisation</h2>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="auto-preview" checked>
<label class="form-check-label" for="auto-preview">Mise à jour automatique</label>
</div>
</div>
<div id="preview-content" class="preview-content"></div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Modal pour les personnages -->
<div class="modal fade" id="characterModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Personnage</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="character-name" class="form-label">Nom</label>
<input type="text" class="form-control" id="character-name">
</div>
<div class="mb-3">
<label for="character-content" class="form-label">Contenu</label>
<textarea class="form-control" id="character-content" rows="10"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="button" class="btn btn-primary" id="save-character">Enregistrer</button>
</div>
</div>
</div>
</div>
<!-- Modal pour les intrigues -->
<div class="modal fade" id="plotModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Intrigue</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="plot-name" class="form-label">Nom</label>
<input type="text" class="form-control" id="plot-name">
</div>
<div class="mb-3">
<label for="plot-content" class="form-label">Contenu</label>
<textarea class="form-control" id="plot-content" rows="10"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="button" class="btn btn-primary" id="save-plot">Enregistrer</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/lib/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/chart.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/codemirror.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/xml.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/javascript.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/css.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/markdown.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/commonlisp.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lib/scheme.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/mode/org.js') }}"></script>
<script>
// Passer les données des personnages et des intrigues au JavaScript
document.getElementById('characters-data').textContent = JSON.stringify({{ characters| tojson | safe }});
document.getElementById('plots-data').textContent = JSON.stringify({{ plots| tojson | safe }});
document.getElementById('update-btn').addEventListener('click', async () => {
const content = document.getElementById('editor-content').value;
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)}`
});
if (response.ok) {
alert('Contenu mis à jour avec succès !');
}
} catch (error) {
alert('Erreur lors de la mise à jour');
}
});
</script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>