Compare commits

..

No commits in common. "main" and "v.1.1.5" have entirely different histories.

16 changed files with 369 additions and 730 deletions

View File

@ -82,8 +82,7 @@ class AboutImageUploadHandler {
// Retourner le chemin relatif pour l'éditeur // Retourner le chemin relatif pour l'éditeur
return [ return [
'success' => true, 'success' => true,
'url' => $filename, 'url' => 'assets/images/about/' . $filename,
'storage_url' => 'assets/images/about/' . $filename,
'width' => $needsResize ? $newWidth : $width, 'width' => $needsResize ? $newWidth : $width,
'height' => $needsResize ? $newHeight : $height 'height' => $needsResize ? $newHeight : $height
]; ];

View File

@ -1,125 +0,0 @@
<?php
require_once '../../includes/config.php';
require_once '../../includes/auth.php';
require_once '../../includes/stories.php';
// Vérification de l'authentification
if (!Auth::check()) {
http_response_code(401);
exit(json_encode(['error' => 'Non autorisé']));
}
function extractImagePaths($content) {
$paths = [];
// Si le contenu est du JSON (format Delta de Quill)
if (is_string($content) && isJson($content)) {
$delta = json_decode($content, true);
if (isset($delta['ops'])) {
foreach ($delta['ops'] as $op) {
if (isset($op['insert']['image'])) {
$paths[] = normalizeImagePath($op['insert']['image']);
}
}
}
} else {
// Si le contenu est du HTML
preg_match_all('/src=["\']([^"\']+)["\']/', $content, $matches);
if (!empty($matches[1])) {
foreach ($matches[1] as $path) {
$paths[] = normalizeImagePath($path);
}
}
}
return $paths;
}
function normalizeImagePath($path) {
// Supprimer les "../" au début du chemin
$path = preg_replace('/^(?:\.\.\/)+/', '', $path);
return $path;
}
function isJson($string) {
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
try {
$unusedFiles = [];
$usedFiles = [];
$totalSpace = 0;
$freedSpace = 0;
// Collecter tous les fichiers dans le dossier chapters
$chaptersDir = __DIR__ . '/../../assets/images/chapters/';
$allFiles = [];
foreach (glob($chaptersDir . '*', GLOB_ONLYDIR) as $storyDir) {
$storyId = basename($storyDir);
foreach (glob($storyDir . '/*') as $file) {
if (is_file($file)) {
$relativePath = 'assets/images/chapters/' . $storyId . '/' . basename($file);
$allFiles[$relativePath] = $file;
$totalSpace += filesize($file);
}
}
}
// Parcourir tous les romans et leurs chapitres
$stories = Stories::getAll();
foreach ($stories as $story) {
// Vérifier la description du roman
if (!empty($story['description'])) {
$usedFiles = array_merge($usedFiles, extractImagePaths($story['description']));
}
// Vérifier les chapitres
if (!empty($story['chapters'])) {
foreach ($story['chapters'] as $chapter) {
if (!empty($chapter['content'])) {
$usedFiles = array_merge($usedFiles, extractImagePaths($chapter['content']));
}
}
}
}
// Identifier les fichiers non utilisés
foreach ($allFiles as $relativePath => $fullPath) {
if (!in_array($relativePath, $usedFiles)) {
$unusedFiles[] = [
'path' => $relativePath,
'size' => filesize($fullPath)
];
$freedSpace += filesize($fullPath);
// Supprimer le fichier
unlink($fullPath);
}
}
// Nettoyer les dossiers vides
foreach (glob($chaptersDir . '*', GLOB_ONLYDIR) as $storyDir) {
if (count(glob("$storyDir/*")) === 0) {
rmdir($storyDir);
}
}
echo json_encode([
'success' => true,
'stats' => [
'filesRemoved' => count($unusedFiles),
'totalSpace' => $totalSpace,
'freedSpace' => $freedSpace,
'details' => $unusedFiles
]
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}

View File

@ -5,20 +5,16 @@ require_once '../../includes/stories.php';
if (!Auth::check()) { if (!Auth::check()) {
http_response_code(401); http_response_code(401);
exit(json_encode(['success' => false, 'error' => 'Non autorisé'])); exit('Non autorisé');
} }
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
$storyId = $input['storyId'] ?? null; $storyId = $input['storyId'] ?? null;
$chapterId = $input['chapterId'] ?? null; $chapterId = $input['chapterId'] ?? null;
$title = $input['title'] ?? ''; $title = $input['title'] ?? '';
$content = $input['content'] ?? ''; $content = $input['content'];
$draft = $input['draft'] ?? false;
try { try {
// Ajout de logs pour déboguer
error_log('Données reçues: ' . print_r($input, true));
$story = Stories::get($storyId); $story = Stories::get($storyId);
if (!$story) { if (!$story) {
throw new Exception('Roman non trouvé'); throw new Exception('Roman non trouvé');
@ -26,42 +22,28 @@ try {
if ($chapterId) { if ($chapterId) {
// Mise à jour d'un chapitre existant // Mise à jour d'un chapitre existant
$chapterUpdated = false;
foreach ($story['chapters'] as &$chapter) { foreach ($story['chapters'] as &$chapter) {
if ($chapter['id'] === $chapterId) { if ($chapter['id'] == $chapterId) {
$chapter['title'] = $title; $chapter['title'] = $title;
$chapter['content'] = $content; $chapter['content'] = $content;
$chapter['draft'] = $draft;
$chapter['updated'] = date('Y-m-d'); $chapter['updated'] = date('Y-m-d');
$chapterUpdated = true;
break; break;
} }
} }
if (!$chapterUpdated) {
throw new Exception('Chapitre non trouvé');
}
} else { } else {
// Nouveau chapitre // Nouveau chapitre
$story['chapters'][] = [ $story['chapters'][] = [
'id' => uniqid(), 'id' => uniqid(),
'title' => $title, 'title' => $title,
'content' => $content, 'content' => $content,
'draft' => $draft,
'created' => date('Y-m-d'), 'created' => date('Y-m-d'),
'updated' => date('Y-m-d') 'updated' => date('Y-m-d')
]; ];
} }
if (!Stories::save($story)) { Stories::save($story);
throw new Exception('Erreur lors de la sauvegarde du roman');
}
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
} catch (Exception $e) { } catch (Exception $e) {
error_log('Erreur dans save-chapter.php: ' . $e->getMessage());
http_response_code(500); http_response_code(500);
echo json_encode([ echo json_encode(['error' => $e->getMessage()]);
'success' => false,
'error' => $e->getMessage()
]);
} }

View File

@ -49,7 +49,6 @@ $stories = Stories::getAll();
<span>Administration</span> <span>Administration</span>
</div> </div>
<div class="nav-menu"> <div class="nav-menu">
<a href="../index.php" target="_blank" class="button">Visiter le site</a>
<a href="profile.php" class="button">Profil</a> <a href="profile.php" class="button">Profil</a>
<a href="story-edit.php" class="button">Nouveau roman</a> <a href="story-edit.php" class="button">Nouveau roman</a>
<a href="options.php" class="button">Options</a> <a href="options.php" class="button">Options</a>

View File

@ -212,14 +212,8 @@ $config = Config::load();
?> ?>
</div> </div>
<button type="button" id="addLink" class="button">Ajouter un lien</button> <button type="button" id="addLink" class="button">Ajouter un lien</button>
<br />
<button type="submit" class="button submit-button">Enregistrer les modifications</button> <button type="submit" class="button submit-button">Enregistrer les modifications</button>
<!-- Section Nettoyage des médias -->
<h2>Maintenance</h2>
<div class="maintenance-actions">
<button type="button" id="cleanMedia" class="button">Nettoyer les médias inutilisés</button>
<small>Supprime les images qui ne sont plus utilisées dans les romans et chapitres.</small>
</div>
</form> </form>
</section> </section>
</div> </div>

View File

@ -149,10 +149,6 @@ function generateSlug($title) {
<div class="modal-header"> <div class="modal-header">
<h2>Éditer un chapitre</h2> <h2>Éditer un chapitre</h2>
<input type="text" id="chapterTitle" placeholder="Titre du chapitre"> <input type="text" id="chapterTitle" placeholder="Titre du chapitre">
<label class="draft-toggle">
<input type="checkbox" id="chapterDraft">
<span class="toggle-label">Mode brouillon</span>
</label>
</div> </div>
<div class="editor-container"> <div class="editor-container">

View File

@ -328,62 +328,6 @@
margin-top: var(--spacing-sm); margin-top: var(--spacing-sm);
} }
/* Styles pour le mode brouillon */
.draft-toggle {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.draft-toggle input[type="checkbox"] {
appearance: none;
width: 40px;
height: 20px;
background: var(--bg-secondary);
border-radius: 10px;
position: relative;
cursor: pointer;
transition: var(--transition-fast);
}
.draft-toggle input[type="checkbox"]::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--text-primary);
left: 2px;
top: 2px;
transition: var(--transition-fast);
}
.draft-toggle input[type="checkbox"]:checked {
background: var(--accent-primary);
}
.draft-toggle input[type="checkbox"]:checked::before {
transform: translateX(20px);
}
.chapter-item.draft {
opacity: 0.7;
border-style: dashed;
}
.chapter-item.draft::after {
content: 'Brouillon';
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: var(--accent-primary);
color: var(--text-tertiary);
padding: 0.2rem 0.5rem;
border-radius: var(--radius-sm);
font-size: 0.8rem;
}
/* Media queries */ /* Media queries */
@media (max-width: 768px) { @media (max-width: 768px) {
.story-cover { .story-cover {

View File

@ -2,8 +2,7 @@
/* Conteneur principal du contenu */ /* Conteneur principal du contenu */
.chapter-content, .chapter-content,
.novel-description, .novel-description {
.about-description {
font-size: 1.1rem; font-size: 1.1rem;
line-height: 1.8; line-height: 1.8;
color: var(--text-primary); color: var(--text-primary);
@ -11,7 +10,6 @@
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
overflow-wrap: break-word; overflow-wrap: break-word;
margin-bottom: 20px;
} }
/* Titres */ /* Titres */
@ -26,47 +24,34 @@
.novel-description h3, .novel-description h3,
.novel-description h4, .novel-description h4,
.novel-description h5, .novel-description h5,
.novel-description h6, .novel-description h6 {
.about-description h1,
.about-description h2,
.about-description h3,
.about-description h4,
.about-description h5,
.about-description h6 {
margin: 1.5em 0 0.8em; margin: 1.5em 0 0.8em;
line-height: 1.3; line-height: 1.3;
color: var(--text-primary); color: var(--text-primary);
} }
.chapter-content h1, .chapter-content h1,
.novel-description h1, .novel-description h1 { font-size: 2em; }
.about-description h1 { font-size: 2em; }
.chapter-content h2, .chapter-content h2,
.novel-description h2, .novel-description h2 { font-size: 1.75em; }
.about-description h2 { font-size: 1.75em; }
.chapter-content h3, .chapter-content h3,
.novel-description h3, .novel-description h3 { font-size: 1.5em; }
.about-description h3 { font-size: 1.5em; }
.chapter-content h4, .chapter-content h4,
.novel-description h4, .novel-description h4 { font-size: 1.25em; }
.about-description h4 { font-size: 1.25em; }
.chapter-content h5, .chapter-content h5,
.novel-description h5, .novel-description h5 { font-size: 1.1em; }
.about-description h5 { font-size: 1.1em; }
.chapter-content h6, .chapter-content h6,
.novel-description h6, .novel-description h6 { font-size: 1em; }
.about-description h6 { font-size: 1em; }
/* Paragraphes et espacement */ /* Paragraphes et espacement */
.chapter-content p, .chapter-content p,
.novel-description p, .novel-description p {
.about-description p { margin: 0 0 0em 0;
margin: 0em 0;
min-height: 1.5em; min-height: 1.5em;
} }
@ -74,25 +59,21 @@
.chapter-content ul, .chapter-content ul,
.chapter-content ol, .chapter-content ol,
.novel-description ul, .novel-description ul,
.novel-description ol, .novel-description ol {
.about-description ul,
.about-description ol {
margin: 1em 0; margin: 1em 0;
padding-left: 2em; padding-left: 2em;
list-style-position: outside; list-style-position: outside;
} }
.chapter-content li, .chapter-content li,
.novel-description li, .novel-description li {
.about-description li {
margin: 0.5em 0; margin: 0.5em 0;
padding-left: 0.5em; padding-left: 0.5em;
} }
/* Citations */ /* Citations */
.chapter-content blockquote, .chapter-content blockquote,
.novel-description blockquote, .novel-description blockquote {
.about-description blockquote {
margin: 1.5em 0; margin: 1.5em 0;
padding: 1em 1.5em; padding: 1em 1.5em;
border-left: 4px solid var(--accent-primary); border-left: 4px solid var(--accent-primary);
@ -104,8 +85,7 @@
/* Blocs de code */ /* Blocs de code */
.chapter-content pre, .chapter-content pre,
.novel-description pre, .novel-description pre {
.about-description pre {
margin: 1.5em 0; margin: 1.5em 0;
padding: 1em; padding: 1em;
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
@ -117,8 +97,7 @@
} }
.chapter-content code, .chapter-content code,
.novel-description code, .novel-description code {
.about-description code {
font-family: "Consolas", "Monaco", monospace; font-family: "Consolas", "Monaco", monospace;
font-size: 0.9em; font-size: 0.9em;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
@ -127,109 +106,28 @@
max-width: 100%; max-width: 100%;
} }
/* Images et alignements */
.chapter-content .ql-align-left,
.novel-description .ql-align-left,
.about-description .ql-align-left {
text-align: left !important;
}
.chapter-content .ql-align-center,
.novel-description .ql-align-center,
.about-description .ql-align-center {
text-align: center !important;
}
.chapter-content .ql-align-right,
.novel-description .ql-align-right,
.about-description .ql-align-right {
text-align: right !important;
}
.chapter-content .ql-align-justify,
.novel-description .ql-align-justify,
.about-description .ql-align-justify {
text-align: justify !important;
}
/* Images */ /* Images */
.chapter-content p img, .chapter-content img,
.novel-description p img, .novel-description img {
.about-description p img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
margin: 1.5em auto; margin: 1.5em 0;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
display: inline-block;
vertical-align: middle;
}
/* Ajustements spécifiques pour les images selon l'alignement */
.ql-align-left p img {
margin-left: 0;
margin-right: auto;
}
.ql-align-center p img {
margin-left: auto;
margin-right: auto;
display: block; display: block;
} }
.ql-align-right p img { /* Alignements */
margin-left: auto; .chapter-content [style*="text-align"],
margin-right: 0; .novel-description [style*="text-align"],
display: block; .novel-description p[style*="text-align"] {
display: block !important;
margin: 1em 0 !important;
} }
/* Support des tailles d'images */ .novel-description .font-serif,
.ql-size-small img { .novel-description .font-sans,
max-width: 50% !important; .novel-description .font-mono {
} display: inline-block !important;
.ql-size-large img {
max-width: 100% !important;
}
/* Styles pour les liens */
.chapter-content a,
.novel-description a,
.about-description a {
color: var(--accent-primary);
text-decoration: none;
transition: color var(--transition-fast);
}
.chapter-content a:hover,
.novel-description a:hover,
.about-description a:hover {
color: var(--accent-secondary);
text-decoration: underline;
}
/* Styles pour les indices et exposants */
.chapter-content sub,
.novel-description sub,
.about-description sub {
vertical-align: sub;
font-size: smaller;
}
.chapter-content sup,
.novel-description sup,
.about-description sup {
vertical-align: super;
font-size: smaller;
}
/* Barre de séparation */
.chapter-divider {
margin: 2em auto;
border: none;
border-top: 2px solid var(--accent-primary);
opacity: 0.5;
width: 100%;
display: block;
} }
/* Polices */ /* Polices */
@ -245,37 +143,68 @@
font-family: "Consolas", "Monaco", monospace !important; font-family: "Consolas", "Monaco", monospace !important;
} }
/* Barre de séparation */
.chapter-divider {
margin: 2em auto;
border: none;
border-top: 2px solid var(--accent-primary);
opacity: 0.5;
width: 100%;
display: block;
}
/* Styles pour les liens */
.chapter-content a,
.novel-description a {
color: var(--accent-primary);
text-decoration: none;
transition: color var(--transition-fast);
}
.chapter-content a:hover,
.novel-description a:hover {
color: var(--accent-secondary);
text-decoration: underline;
}
/* Styles pour les indices et exposants */
.chapter-content sub,
.novel-description sub {
vertical-align: sub;
font-size: smaller;
}
.chapter-content sup,
.novel-description sup {
vertical-align: super;
font-size: smaller;
}
/* Media queries pour le responsive */ /* Media queries pour le responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.chapter-content, .chapter-content,
.novel-description, .novel-description {
.about-description {
font-size: 1rem; font-size: 1rem;
} }
.chapter-content blockquote, .chapter-content blockquote,
.novel-description blockquote, .novel-description blockquote {
.about-description blockquote {
margin: 1em 0; margin: 1em 0;
padding: 0.8em 1em; padding: 0.8em 1em;
} }
.chapter-content pre, .chapter-content pre,
.novel-description pre, .novel-description pre {
.about-description pre {
padding: 0.8em; padding: 0.8em;
font-size: 0.85em; font-size: 0.85em;
} }
.chapter-content h1, .chapter-content h1,
.novel-description h1, .novel-description h1 { font-size: 1.75em; }
.about-description h1 { font-size: 1.75em; }
.chapter-content h2, .chapter-content h2,
.novel-description h2, .novel-description h2 { font-size: 1.5em; }
.about-description h2 { font-size: 1.5em; }
.chapter-content h3, .chapter-content h3,
.novel-description h3, .novel-description h3 { font-size: 1.25em; }
.about-description h3 { font-size: 1.25em; }
} }

View File

@ -185,23 +185,8 @@
/* Bouton de soumission */ /* Bouton de soumission */
.submit-button { .submit-button {
margin-top: var(--spacing-xl); margin-top: var(--spacing-xl);
} width: 100%;
/* Nettoyage des médias */
.maintenance-actions {
margin: var(--spacing-lg) 0;
padding: var(--spacing-md); padding: var(--spacing-md);
background: var(--bg-secondary);
border-radius: var(--radius-sm);
}
.maintenance-actions button {
margin-bottom: var(--spacing-xs);
}
.maintenance-actions small {
display: block;
color: var(--text-secondary);
} }
/* Responsive */ /* Responsive */

View File

@ -108,7 +108,6 @@ body {
position: absolute; position: absolute;
top: var(--spacing-md); top: var(--spacing-md);
right: var(--spacing-md); right: var(--spacing-md);
z-index: 2;
} }
.about-button { .about-button {

View File

@ -42,18 +42,7 @@ document.addEventListener('DOMContentLoaded', function() {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
const range = aboutEditor.getSelection(true); const range = aboutEditor.getSelection(true);
// Utiliser le chemin complet pour l'affichage aboutEditor.insertEmbed(range.index, 'image', result.url);
aboutEditor.insertEmbed(range.index, 'image', '../' + result.storage_url);
// Mettre à jour le contenu Delta si nécessaire
const insertOp = aboutEditor.getContents().ops.find(op =>
op.insert && op.insert.image === '../' + result.storage_url
);
if (insertOp) {
// Stocker le chemin relatif
insertOp.insert.image = result.storage_url;
}
aboutEditor.setSelection(range.index + 1); aboutEditor.setSelection(range.index + 1);
} else { } else {
showNotification(result.error || 'Erreur lors de l\'upload', 'error'); showNotification(result.error || 'Erreur lors de l\'upload', 'error');
@ -192,36 +181,4 @@ document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => notification.remove(), 300); setTimeout(() => notification.remove(), 300);
}, 3000); }, 3000);
} }
// Gestion du nettoyage des médias
const cleanMediaBtn = document.getElementById('cleanMedia');
if (cleanMediaBtn) {
cleanMediaBtn.addEventListener('click', async () => {
confirmDialog.show({
title: 'Nettoyage des médias',
message: 'Voulez-vous vraiment supprimer toutes les images qui ne sont plus utilisées ? Cette action est irréversible.',
confirmText: 'Nettoyer',
confirmClass: 'danger',
onConfirm: async () => {
try {
const response = await fetch('api/clean-media.php');
if (!response.ok) throw new Error('Erreur réseau');
const result = await response.json();
if (result.success) {
const stats = result.stats;
const mbFreed = (stats.freedSpace / (1024 * 1024)).toFixed(2);
const message = `${stats.filesRemoved} fichier(s) supprimé(s).\n${mbFreed} Mo d'espace libéré.`;
showNotification(message);
} else {
throw new Error(result.error || 'Une erreur est survenue');
}
} catch (error) {
console.error('Erreur:', error);
showNotification(error.message, 'error');
}
}
});
});
}
}); });

View File

@ -1,29 +1,9 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Récupération des paramètres et variables globales
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const storyId = document.querySelector('input[name="id"]')?.value || urlParams.get('id'); const storyId = document.querySelector('input[name="id"]')?.value || urlParams.get('id');
let currentChapterId = null; let currentChapterId = null;
let hasUnsavedChanges = false;
// Références DOM pour l'éditeur // Fonction de notification
const modal = document.getElementById('chapterEditor');
const coverModal = document.getElementById('chapterCoverEditor');
const addChapterBtn = document.getElementById('addChapter');
const saveChapterBtn = document.getElementById('saveChapter');
const cancelEditBtn = document.getElementById('cancelEdit');
const chapterTitleInput = document.getElementById('chapterTitle');
const chaptersList = document.getElementById('chaptersList');
const chapterDraftToggle = document.getElementById('chapterDraft');
// Éléments pour la modale de couverture
const coverPreview = document.querySelector('.current-cover-preview');
const chapterCoverInput = document.getElementById('chapterCover');
const saveCoverBtn = document.getElementById('saveCover');
const cancelCoverBtn = document.getElementById('cancelCoverEdit');
const deleteCoverBtn = document.getElementById('deleteCover');
let currentChapterCover = null;
// Notification système
function showNotification(message, type = 'success') { function showNotification(message, type = 'success') {
const notification = document.createElement('div'); const notification = document.createElement('div');
notification.className = `notification ${type}`; notification.className = `notification ${type}`;
@ -31,11 +11,13 @@ document.addEventListener('DOMContentLoaded', function() {
document.body.appendChild(notification); document.body.appendChild(notification);
// Animation d'entrée
setTimeout(() => { setTimeout(() => {
notification.style.opacity = '1'; notification.style.opacity = '1';
notification.style.transform = 'translateY(0)'; notification.style.transform = 'translateY(0)';
}, 10); }, 10);
// Auto-suppression après 3 secondes
setTimeout(() => { setTimeout(() => {
notification.style.opacity = '0'; notification.style.opacity = '0';
notification.style.transform = 'translateY(-100%)'; notification.style.transform = 'translateY(-100%)';
@ -43,7 +25,33 @@ document.addEventListener('DOMContentLoaded', function() {
}, 3000); }, 3000);
} }
// Configuration de l'éditeur Quill // Création de l'icône SVG pour le séparateur
const icons = {
divider: `
<svg viewBox="0 0 18 18">
<line class="ql-stroke" x1="3" x2="15" y1="9" y2="9"></line>
</svg>
`
};
// Ajout de l'icône à Quill
const Block = Quill.import('blots/block');
const icons_list = Quill.import('ui/icons');
icons_list['divider'] = icons.divider;
// Définition du format pour le séparateur
class DividerBlot extends Block {
static create() {
const node = super.create();
node.className = 'chapter-divider';
return node;
}
}
DividerBlot.blotName = 'divider';
DividerBlot.tagName = 'hr';
Quill.register(DividerBlot);
// Configuration et initialisation de Quill
const quill = new Quill('#editor', { const quill = new Quill('#editor', {
theme: 'snow', theme: 'snow',
modules: { modules: {
@ -107,241 +115,39 @@ document.addEventListener('DOMContentLoaded', function() {
}; };
} }
} }
},
keyboard: {
bindings: {
tab: false,
'indent backwards': false
}
} }
}, },
placeholder: 'Commencez à écrire votre chapitre ici...' placeholder: 'Commencez à écrire votre chapitre ici...'
}); });
// Détection des changements non sauvegardés // Gestion des chapitres
function detectUnsavedChanges() { const modal = document.getElementById('chapterEditor');
quill.on('text-change', () => { const addChapterBtn = document.getElementById('addChapter');
hasUnsavedChanges = true; const saveChapterBtn = document.getElementById('saveChapter');
}); const cancelEditBtn = document.getElementById('cancelEdit');
const chapterTitleInput = document.getElementById('chapterTitle');
const chaptersList = document.getElementById('chaptersList');
if (chapterTitleInput) { // Éléments de la modale de couverture
chapterTitleInput.addEventListener('input', () => { const coverModal = document.getElementById('chapterCoverEditor');
hasUnsavedChanges = true; const coverPreview = document.querySelector('.current-cover-preview');
}); const chapterCoverInput = document.getElementById('chapterCover');
} const saveCoverBtn = document.getElementById('saveCover');
} const cancelCoverBtn = document.getElementById('cancelCoverEdit');
const deleteCoverBtn = document.getElementById('deleteCover');
let currentChapterCover = null;
// Configuration de Sortable pour la réorganisation des chapitres // Gestionnaire pour le bouton de couverture
if (chaptersList) {
new Sortable(chaptersList, {
animation: 150,
handle: '.chapter-number',
ghostClass: 'sortable-ghost',
onEnd: async function(evt) {
const chapters = Array.from(chaptersList.children).map((item, index) => ({
id: item.dataset.id,
position: index
}));
try {
const response = await fetch('api/reorder-chapters.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
storyId: storyId,
chapters: chapters
})
});
if (!response.ok) {
throw new Error('Erreur lors de la réorganisation');
}
chapters.forEach((chapter, index) => {
const element = document.querySelector(`[data-id="${chapter.id}"] .chapter-number`);
if (element) {
element.textContent = index + 1;
}
});
} catch (error) {
console.error('Erreur:', error);
showNotification('Erreur lors de la réorganisation des chapitres', 'error');
}
}
});
}
// Gestion de la sauvegarde du chapitre
if (saveChapterBtn) {
saveChapterBtn.addEventListener('click', async () => {
const title = chapterTitleInput.value.trim();
if (!title) {
showNotification('Le titre est requis', 'error');
return;
}
try {
const response = await fetch('api/save-chapter.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
storyId: storyId,
chapterId: currentChapterId,
title: title,
content: quill.root.innerHTML,
draft: chapterDraftToggle?.checked || false
})
});
if (response.ok) {
const result = await response.json();
if (result.success) {
hasUnsavedChanges = false;
showNotification('Chapitre sauvegardé avec succès');
setTimeout(() => location.reload(), 500);
} else {
throw new Error(result.error || 'Erreur lors de la sauvegarde');
}
} else {
throw new Error('Erreur réseau');
}
} catch (error) {
console.error('Erreur:', error);
showNotification('Erreur lors de la sauvegarde du chapitre', 'error');
}
});
}
// Gestionnaires d'événements pour l'édition des chapitres
if (addChapterBtn) {
addChapterBtn.addEventListener('click', () => {
currentChapterId = null;
chapterTitleInput.value = '';
quill.setContents([]);
if (chapterDraftToggle) {
chapterDraftToggle.checked = false;
}
hasUnsavedChanges = false;
modal.style.display = 'block';
});
}
if (cancelEditBtn) {
cancelEditBtn.addEventListener('click', () => {
if (hasUnsavedChanges) {
confirmDialog.show({
title: 'Modifications non sauvegardées',
message: 'Des modifications non sauvegardées seront perdues. Voulez-vous vraiment fermer ?',
confirmText: 'Fermer sans sauvegarder',
onConfirm: () => {
modal.style.display = 'none';
hasUnsavedChanges = false;
}
});
} else {
modal.style.display = 'none';
}
});
}
// Gestion des clics sur la liste des chapitres
if (chaptersList) { if (chaptersList) {
chaptersList.addEventListener('click', async (e) => { chaptersList.addEventListener('click', async (e) => {
const target = e.target; if (e.target.matches('.edit-cover')) {
const chapterItem = e.target.closest('.chapter-item');
if (target.matches('.edit-chapter')) {
const chapterItem = target.closest('.chapter-item');
currentChapterId = chapterItem.dataset.id;
try {
const response = await fetch(`api/get-chapter.php?storyId=${storyId}&chapterId=${currentChapterId}`);
if (!response.ok) {
throw new Error('Erreur réseau');
}
const chapter = await response.json();
// Mise à jour du titre
chapterTitleInput.value = chapter.title || '';
// Gestion du contenu
if (chapter.html) {
quill.root.innerHTML = chapter.html;
} else if (chapter.content) {
try {
const content = typeof chapter.content === 'string'
? JSON.parse(chapter.content)
: chapter.content;
quill.setContents(content);
} catch (contentError) {
console.error('Erreur de parsing du contenu:', contentError);
quill.setText(chapter.content || '');
}
} else {
quill.setContents([]);
}
// Mise à jour du mode brouillon
if (chapterDraftToggle) {
chapterDraftToggle.checked = chapter.draft || false;
}
modal.style.display = 'block';
hasUnsavedChanges = false;
} catch (error) {
console.error('Erreur détaillée:', error);
showNotification('Erreur lors du chargement du chapitre', 'error');
}
}
// Gestion de la suppression
if (target.matches('.delete-chapter')) {
const chapterItem = target.closest('.chapter-item');
const chapterId = chapterItem.dataset.id;
const chapterTitle = chapterItem.querySelector('.chapter-title').textContent;
confirmDialog.show({
title: 'Suppression du chapitre',
message: `Voulez-vous vraiment supprimer le chapitre "${chapterTitle}" ? Cette action est irréversible.`,
confirmText: 'Supprimer',
confirmClass: 'danger',
onConfirm: async () => {
try {
const response = await fetch('api/delete-chapter.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
storyId: storyId,
chapterId: chapterId
})
});
if (!response.ok) throw new Error('Erreur réseau');
const result = await response.json();
if (result.success) {
chapterItem.style.opacity = '0';
chapterItem.style.transform = 'translateX(-100%)';
setTimeout(() => {
chapterItem.remove();
showNotification('Chapitre supprimé avec succès');
}, 300);
} else {
throw new Error(result.error || 'Erreur lors de la suppression');
}
} catch (error) {
console.error('Erreur:', error);
showNotification(error.message, 'error');
}
}
});
}
// Gestion de la couverture
if (target.matches('.edit-cover')) {
const chapterItem = target.closest('.chapter-item');
const chapterId = chapterItem.dataset.id; const chapterId = chapterItem.dataset.id;
const chapterTitle = chapterItem.querySelector('.chapter-title').textContent; const chapterTitle = chapterItem.querySelector('.chapter-title').textContent;
@ -373,7 +179,7 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// Prévisualisation de l'image de couverture // Prévisualisation de l'image
if (chapterCoverInput) { if (chapterCoverInput) {
chapterCoverInput.addEventListener('change', (e) => { chapterCoverInput.addEventListener('change', (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
@ -425,7 +231,6 @@ document.addEventListener('DOMContentLoaded', function() {
showNotification('Couverture mise à jour avec succès'); showNotification('Couverture mise à jour avec succès');
coverModal.style.display = 'none'; coverModal.style.display = 'none';
chapterCoverInput.value = ''; chapterCoverInput.value = '';
setTimeout(() => location.reload(), 500);
} else { } else {
throw new Error(result.error || 'Erreur lors de la mise à jour'); throw new Error(result.error || 'Erreur lors de la mise à jour');
} }
@ -465,7 +270,6 @@ document.addEventListener('DOMContentLoaded', function() {
showNotification('Couverture supprimée avec succès'); showNotification('Couverture supprimée avec succès');
coverPreview.innerHTML = ''; coverPreview.innerHTML = '';
coverModal.style.display = 'none'; coverModal.style.display = 'none';
setTimeout(() => location.reload(), 500);
} else { } else {
throw new Error(result.error || 'Erreur lors de la suppression'); throw new Error(result.error || 'Erreur lors de la suppression');
} }
@ -478,6 +282,208 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// Initialisation // Configuration de Sortable pour la réorganisation des chapitres
detectUnsavedChanges(); if (chaptersList) {
new Sortable(chaptersList, {
animation: 150,
handle: '.chapter-number',
ghostClass: 'sortable-ghost',
onEnd: async function(evt) {
const chapters = Array.from(chaptersList.children).map((item, index) => ({
id: item.dataset.id,
position: index
}));
try {
const response = await fetch('api/reorder-chapters.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
storyId: storyId,
chapters: chapters
})
});
if (!response.ok) {
throw new Error('Erreur lors de la réorganisation');
}
chapters.forEach((chapter, index) => {
const element = document.querySelector(`[data-id="${chapter.id}"] .chapter-number`);
if (element) {
element.textContent = index + 1;
}
});
} catch (error) {
console.error('Erreur:', error);
showNotification('Erreur lors de la réorganisation des chapitres', 'error');
}
}
});
}
// Gestionnaires d'événements pour l'édition des chapitres
if (addChapterBtn) {
addChapterBtn.addEventListener('click', () => {
currentChapterId = null;
chapterTitleInput.value = '';
quill.setContents([]);
modal.style.display = 'block';
});
}
if (cancelEditBtn) {
cancelEditBtn.addEventListener('click', () => {
if (hasUnsavedChanges()) {
confirmDialog.show({
title: 'Modifications non sauvegardées',
message: 'Des modifications non sauvegardées seront perdues. Voulez-vous vraiment fermer ?',
confirmText: 'Fermer sans sauvegarder',
onConfirm: () => {
modal.style.display = 'none';
}
});
} else {
modal.style.display = 'none';
}
});
}
// Gestion des clics sur la liste des chapitres
if (chaptersList) {
chaptersList.addEventListener('click', async (e) => {
const target = e.target;
if (target.matches('.edit-chapter')) {
const chapterItem = target.closest('.chapter-item');
currentChapterId = chapterItem.dataset.id;
try {
const response = await fetch(`api/get-chapter.php?storyId=${storyId}&chapterId=${currentChapterId}`);
if (!response.ok) {
throw new Error('Erreur réseau');
}
const chapter = await response.json();
// Mise à jour du titre
chapterTitleInput.value = chapter.title || '';
// Gestion du contenu
if (chapter.html) {
quill.root.innerHTML = chapter.html;
} else if (chapter.content) {
try {
const content = typeof chapter.content === 'string'
? JSON.parse(chapter.content)
: chapter.content;
quill.setContents(content);
} catch (contentError) {
console.error('Erreur de parsing du contenu:', contentError);
quill.setText(chapter.content || '');
}
} else {
quill.setContents([]);
}
modal.style.display = 'block';
} catch (error) {
console.error('Erreur détaillée:', error);
showNotification('Erreur lors du chargement du chapitre', 'error');
}
}
// Gestion de la suppression
if (target.matches('.delete-chapter')) {
const chapterItem = target.closest('.chapter-item');
const chapterId = chapterItem.dataset.id;
const chapterTitle = chapterItem.querySelector('.chapter-title').textContent;
confirmDialog.show({
title: 'Suppression du chapitre',
message: `Voulez-vous vraiment supprimer le chapitre "${chapterTitle}" ? Cette action est irréversible.`,
confirmText: 'Supprimer',
confirmClass: 'danger',
onConfirm: async () => {
try {
const response = await fetch('api/delete-chapter.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
storyId: storyId,
chapterId: chapterId
})
});
if (!response.ok) throw new Error('Erreur réseau');
const result = await response.json();
if (result.success) {
chapterItem.style.opacity = '0';
chapterItem.style.transform = 'translateX(-100%)';
setTimeout(() => {
chapterItem.remove();
showNotification('Chapitre supprimé avec succès');
}, 300);
} else {
throw new Error(result.error || 'Erreur lors de la suppression');
}
} catch (error) {
console.error('Erreur:', error);
showNotification(error.message, 'error');
}
}
});
}
});
}
// Sauvegarde d'un chapitre
if (saveChapterBtn) {
saveChapterBtn.addEventListener('click', async () => {
const title = chapterTitleInput.value.trim();
if (!title) {
showNotification('Le titre est requis', 'error');
return;
}
try {
const response = await fetch('api/save-chapter.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
storyId: storyId,
chapterId: currentChapterId,
title: title,
content: quill.root.innerHTML
})
});
if (response.ok) {
showNotification('Chapitre sauvegardé avec succès');
setTimeout(() => location.reload(), 500);
} else {
throw new Error('Erreur lors de la sauvegarde');
}
} catch (error) {
console.error('Erreur:', error);
showNotification('Erreur lors de la sauvegarde du chapitre', 'error');
}
});
}
function hasUnsavedChanges() {
if (!currentChapterId) {
return chapterTitleInput.value !== '' || quill.getLength() > 1;
}
return true;
}
}); });

View File

@ -1,6 +1,5 @@
<?php <?php
require_once 'includes/config.php'; require_once 'includes/config.php';
require_once 'includes/auth.php';
require_once 'includes/stories.php'; require_once 'includes/stories.php';
// Récupération des paramètres // Récupération des paramètres
@ -23,9 +22,6 @@ if (!$story) {
$currentChapter = null; $currentChapter = null;
$currentIndex = -1; $currentIndex = -1;
foreach ($story['chapters'] as $index => $chapter) { foreach ($story['chapters'] as $index => $chapter) {
if (($chapter['draft'] ?? false) && !Auth::check()) {
continue;
}
if ($chapter['id'] === $chapterId) { if ($chapter['id'] === $chapterId) {
$currentChapter = $chapter; $currentChapter = $chapter;
$currentIndex = $index; $currentIndex = $index;
@ -39,14 +35,8 @@ if (!$currentChapter) {
} }
// Récupération des chapitres précédent et suivant // Récupération des chapitres précédent et suivant
$visibleChapters = array_filter($story['chapters'], function($ch) { $prevChapter = $currentIndex > 0 ? $story['chapters'][$currentIndex - 1] : null;
return !($ch['draft'] ?? false) || Auth::check(); $nextChapter = $currentIndex < count($story['chapters']) - 1 ? $story['chapters'][$currentIndex + 1] : null;
});
$visibleChapters = array_values($visibleChapters);
$currentVisibleIndex = array_search($currentChapter, $visibleChapters);
$prevChapter = $currentVisibleIndex > 0 ? $visibleChapters[$currentVisibleIndex - 1] : null;
$nextChapter = $currentVisibleIndex < count($visibleChapters) - 1 ? $visibleChapters[$currentVisibleIndex + 1] : null;
$config = Config::load(); $config = Config::load();
?> ?>
@ -72,10 +62,6 @@ $config = Config::load();
<?php if (!empty($currentChapter['cover'])): ?> <?php if (!empty($currentChapter['cover'])): ?>
<div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($currentChapter['cover']) ?>');"></div> <div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($currentChapter['cover']) ?>');"></div>
<?php endif; ?> <?php endif; ?>
<div class="header-actions">
<a href="index.php" class="about-button">Accueil</a>
<a href="roman.php?id=<?= urlencode($storyId) ?>" class="about-button">Roman</a>
</div>
<h1><?= htmlspecialchars($currentChapter['title']) ?></h1> <h1><?= htmlspecialchars($currentChapter['title']) ?></h1>
</header> </header>
@ -105,16 +91,10 @@ $config = Config::load();
<aside class="chapters-menu"> <aside class="chapters-menu">
<h2>Chapitres</h2> <h2>Chapitres</h2>
<ul class="chapters-list"> <ul class="chapters-list">
<?php <?php foreach ($story['chapters'] as $chapter): ?>
$visibleChapters = array_filter($story['chapters'], function($chapter) {
return !($chapter['draft'] ?? false) || Auth::check();
});
foreach ($visibleChapters as $chapter):
?>
<li> <li>
<a href="?story=<?= urlencode($storyId) ?>&chapter=<?= urlencode($chapter['id']) ?>" <a href="?story=<?= urlencode($storyId) ?>&chapter=<?= urlencode($chapter['id']) ?>"
class="<?= $chapter['id'] === $chapterId ? 'current-chapter' : '' ?>"> class="<?= $chapter['id'] === $chapterId ? 'current-chapter' : '' ?>">
<?= htmlspecialchars($chapter['title']) ?> <?= htmlspecialchars($chapter['title']) ?>
</a> </a>
</li> </li>
@ -123,6 +103,11 @@ $config = Config::load();
</aside> </aside>
</div> </div>
<div class="back-to-home">
<a href="index.php">&larr; Retour à l'accueil</a> |
<a href="roman.php?id=<?= urlencode($storyId) ?>">&larr; Retour au roman</a>
</div>
<button class="scroll-top" aria-label="Retour en haut de page"></button> <button class="scroll-top" aria-label="Retour en haut de page"></button>
<script> <script>

View File

@ -1,6 +1,5 @@
<?php <?php
require_once 'includes/config.php'; require_once 'includes/config.php';
require_once 'includes/auth.php';
require_once 'includes/stories.php'; require_once 'includes/stories.php';
$config = Config::load(); $config = Config::load();
@ -78,22 +77,16 @@ function formatDate($date) {
class="novel-cover" class="novel-cover"
loading="lazy"> loading="lazy">
<div class="novel-info"> <div class="novel-info">
<h2><?= htmlspecialchars($story['title']) ?></h2> <h2><?= htmlspecialchars($story['title']) ?></h2>
<?php <p>
$visibleChapters = array_filter($story['chapters'] ?? [], function($chapter) { <?= count($story['chapters'] ?? []) ?>
return !($chapter['draft'] ?? false) || Auth::check(); chapitre<?= count($story['chapters'] ?? []) > 1 ? 's' : '' ?>
}); </p>
$chapterCount = count($visibleChapters); <div class="novel-date">
?> Mis à jour le <?= formatDate($story['updated']) ?>
<p>
<?= $chapterCount ?>
chapitre<?= $chapterCount > 1 ? 's' : '' ?>
</p>
<div class="novel-date">
Mis à jour le <?= formatDate($story['updated']) ?>
</div>
</div> </div>
</div>
</a> </a>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>

View File

@ -1,6 +1,5 @@
<?php <?php
require_once 'includes/config.php'; require_once 'includes/config.php';
require_once 'includes/auth.php';
require_once 'includes/stories.php'; require_once 'includes/stories.php';
// Récupération de l'ID du roman depuis l'URL // Récupération de l'ID du roman depuis l'URL
@ -41,9 +40,6 @@ $config = Config::load();
<?php if (!empty($story['cover'])): ?> <?php if (!empty($story['cover'])): ?>
<div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($story['cover']) ?>');"></div> <div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($story['cover']) ?>');"></div>
<?php endif; ?> <?php endif; ?>
<div class="header-actions">
<a href="index.php" class="about-button">Accueil</a>
</div>
<h1><?= htmlspecialchars($story['title']) ?></h1> <h1><?= htmlspecialchars($story['title']) ?></h1>
</header> </header>
@ -57,13 +53,7 @@ $config = Config::load();
<h2>Chapitres</h2> <h2>Chapitres</h2>
<?php if (!empty($story['chapters'])): ?> <?php if (!empty($story['chapters'])): ?>
<ul class="chapters-list"> <ul class="chapters-list">
<?php <?php foreach ($story['chapters'] as $index => $chapter): ?>
$visibleChapters = array_filter($story['chapters'], function($chapter) {
return !($chapter['draft'] ?? false) || Auth::check();
});
foreach ($visibleChapters as $chapter):
?>
<li> <li>
<a href="chapitre.php?story=<?= urlencode($story['id']) ?>&chapter=<?= urlencode($chapter['id']) ?>"> <a href="chapitre.php?story=<?= urlencode($story['id']) ?>&chapter=<?= urlencode($chapter['id']) ?>">
<?= htmlspecialchars($chapter['title']) ?> <?= htmlspecialchars($chapter['title']) ?>
@ -77,6 +67,12 @@ $config = Config::load();
</aside> </aside>
</div> </div>
<?php if (!empty($config['site']['name'])): ?>
<div class="back-to-home">
<a href="index.php">&larr; Retour à l'accueil</a>
</div>
<?php endif; ?>
<button class="scroll-top" aria-label="Retour en haut de page"></button> <button class="scroll-top" aria-label="Retour en haut de page"></button>
<script> <script>

View File

@ -1 +1 @@
1.2.0 1.1.5