Compare commits

...

4 Commits

7 changed files with 340 additions and 80 deletions

View File

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

125
admin/api/clean-media.php Normal file
View File

@ -0,0 +1,125 @@
<?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

@ -212,8 +212,14 @@ $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

@ -2,7 +2,8 @@
/* 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);
@ -24,34 +25,47 @@
.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 { font-size: 2em; } .novel-description h1,
.about-description h1 { font-size: 2em; }
.chapter-content h2, .chapter-content h2,
.novel-description h2 { font-size: 1.75em; } .novel-description h2,
.about-description h2 { font-size: 1.75em; }
.chapter-content h3, .chapter-content h3,
.novel-description h3 { font-size: 1.5em; } .novel-description h3,
.about-description h3 { font-size: 1.5em; }
.chapter-content h4, .chapter-content h4,
.novel-description h4 { font-size: 1.25em; } .novel-description h4,
.about-description h4 { font-size: 1.25em; }
.chapter-content h5, .chapter-content h5,
.novel-description h5 { font-size: 1.1em; } .novel-description h5,
.about-description h5 { font-size: 1.1em; }
.chapter-content h6, .chapter-content h6,
.novel-description h6 { font-size: 1em; } .novel-description h6,
.about-description h6 { font-size: 1em; }
/* Paragraphes et espacement */ /* Paragraphes et espacement */
.chapter-content p, .chapter-content p,
.novel-description p { .novel-description p,
margin: 0 0 0em 0; .about-description p {
margin: 0em 0;
min-height: 1.5em; min-height: 1.5em;
} }
@ -59,21 +73,25 @@
.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);
@ -85,7 +103,8 @@
/* 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);
@ -97,7 +116,8 @@
} }
.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;
@ -106,28 +126,109 @@
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 img, .chapter-content p img,
.novel-description img { .novel-description p img,
.about-description p img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
margin: 1.5em 0; margin: 1.5em auto;
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;
} }
/* Alignements */ .ql-align-right p img {
.chapter-content [style*="text-align"], margin-left: auto;
.novel-description [style*="text-align"], margin-right: 0;
.novel-description p[style*="text-align"] { display: block;
display: block !important;
margin: 1em 0 !important;
} }
.novel-description .font-serif, /* Support des tailles d'images */
.novel-description .font-sans, .ql-size-small img {
.novel-description .font-mono { max-width: 50% !important;
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 */
@ -143,68 +244,37 @@
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 { font-size: 1.75em; } .novel-description h1,
.about-description h1 { font-size: 1.75em; }
.chapter-content h2, .chapter-content h2,
.novel-description h2 { font-size: 1.5em; } .novel-description h2,
.about-description h2 { font-size: 1.5em; }
.chapter-content h3, .chapter-content h3,
.novel-description h3 { font-size: 1.25em; } .novel-description h3,
.about-description h3 { font-size: 1.25em; }
} }

View File

@ -185,8 +185,23 @@
/* 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

@ -24,25 +24,36 @@ document.addEventListener('DOMContentLoaded', function() {
input.setAttribute('type', 'file'); input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*'); input.setAttribute('accept', 'image/*');
input.click(); input.click();
input.onchange = async () => { input.onchange = async () => {
const file = input.files[0]; const file = input.files[0];
if (file) { if (file) {
const formData = new FormData(); const formData = new FormData();
formData.append('image', file); formData.append('image', file);
try { try {
const response = await fetch('api/about-image-upload.php', { const response = await fetch('api/about-image-upload.php', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
if (!response.ok) throw new Error('Upload failed'); if (!response.ok) throw new Error('Upload failed');
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);
aboutEditor.insertEmbed(range.index, 'image', result.url); // Utiliser le chemin complet pour l'affichage
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');
@ -181,4 +192,36 @@ 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 +1 @@
1.1.5 1.1.7