Compare commits
No commits in common. "main" and "v.1.1.5" have entirely different histories.
@ -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
|
||||||
];
|
];
|
||||||
|
@ -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()
|
|
||||||
]);
|
|
||||||
}
|
|
@ -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()
|
|
||||||
]);
|
|
||||||
}
|
}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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 {
|
||||||
|
@ -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; }
|
|
||||||
}
|
}
|
@ -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 */
|
||||||
|
@ -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 {
|
||||||
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
@ -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;
|
||||||
|
}
|
||||||
});
|
});
|
33
chapitre.php
33
chapitre.php
@ -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">← Retour à l'accueil</a> |
|
||||||
|
<a href="roman.php?id=<?= urlencode($storyId) ?>">← 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>
|
||||||
|
25
index.php
25
index.php
@ -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>
|
||||||
|
18
roman.php
18
roman.php
@ -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">← 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>
|
||||||
|
@ -1 +1 @@
|
|||||||
1.2.0
|
1.1.5
|
Loading…
x
Reference in New Issue
Block a user