mirror of
https://forge.chapril.org/tykayn/book_generator
synced 2025-06-20 01:34:43 +02:00
ajout app flask pour prévisualiser le livre
This commit is contained in:
parent
375fbb3a7a
commit
9f1b265a21
29 changed files with 4533 additions and 137 deletions
393
app.py
393
app.py
|
@ -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)
|
70
data.json
70
data.json
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
68
livre.org
68
livre.org
|
@ -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
22
personnages.org
Normal 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
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
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
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
1
static/css/lib/eclipse.min.css
vendored
Normal 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
6
static/css/lib/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/css/lib/fonts/bootstrap-icons.woff
Normal file
BIN
static/css/lib/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
static/css/lib/fonts/bootstrap-icons.woff2
Normal file
BIN
static/css/lib/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
1
static/css/lib/monokai.min.css
vendored
Normal file
1
static/css/lib/monokai.min.css
vendored
Normal 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}
|
|
@ -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);
|
||||
}
|
BIN
static/css/webfonts/fa-solid-900.ttf
Normal file
BIN
static/css/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
static/css/webfonts/fa-solid-900.woff2
Normal file
BIN
static/css/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
7
static/js/lib/bootstrap.bundle.min.js
vendored
Normal file
7
static/js/lib/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/lib/bootstrap.bundle.min.js.map
Normal file
1
static/js/lib/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
13
static/js/lib/chart.min.js
vendored
Normal file
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
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
1
static/js/lib/commonlisp.min.js
vendored
Normal 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
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
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
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
83
static/js/lib/org.min.js
vendored
Normal 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
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
1
static/js/lib/xml.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1014
static/js/main.js
1014
static/js/main.js
File diff suppressed because it is too large
Load diff
138
static/js/mode/org.js
Normal file
138
static/js/mode/org.js
Normal 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");
|
||||
});
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue