feat : barre de partage articles + déduplication images uploadées (v1.6.20)

- post_view.php : barre de partage (mail, X, LinkedIn, Mastodon, copier, Web Share) sur articles publiés (#47)
- share.js : logique clipboard + navigator.share sans script tiers, compatible CSP (#47)
- addFile() : hardlink vers fichier identique si même hash16-size.ext dans un autre article (#35)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 10:56:43 +02:00
parent 5ce91da06a
commit 3b22be94e8
6 changed files with 85 additions and 2 deletions
+8
View File
@@ -5,6 +5,14 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag
---
## [1.6.20] - 2026-05-16
### Ajouté
- Barre de partage sur les articles publiés : mail, X, LinkedIn, Mastodon, copie de lien, Web Share API mobile (#47)
- Déduplication des images uploadées par hardlink si même hash+taille existe déjà dans un autre article (#35)
---
## [1.6.19] - 2026-05-16
### Ajouté
+40
View File
@@ -0,0 +1,40 @@
(function () {
'use strict';
var bar = document.getElementById('share-bar');
if (!bar) { return; }
var url = bar.getAttribute('data-url') || window.location.href;
var title = bar.getAttribute('data-title') || document.title;
var copyBtn = document.getElementById('share-copy');
if (copyBtn) {
copyBtn.addEventListener('click', function () {
if (!navigator.clipboard) {
var ta = document.createElement('textarea');
ta.value = url;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
copyBtn.textContent = 'Copié !';
setTimeout(function () { copyBtn.textContent = 'Copier le lien'; }, 2000);
return;
}
navigator.clipboard.writeText(url).then(function () {
copyBtn.textContent = 'Copié !';
setTimeout(function () { copyBtn.textContent = 'Copier le lien'; }, 2000);
});
});
}
var nativeBtn = document.getElementById('share-native');
if (nativeBtn) {
if (navigator.share) {
nativeBtn.hidden = false;
nativeBtn.addEventListener('click', function () {
navigator.share({ title: title, url: url }).catch(function () {});
});
}
}
}());
+1 -1
View File
@@ -1 +1 @@
1.6.19
1.6.20
+10 -1
View File
@@ -1197,7 +1197,16 @@ class ArticleManager
$size = filesize($uploadedFile['tmp_name']);
$name = "{$hash}-{$size}.{$ext}";
$dest = $dir . '/' . $name;
if (!rename($uploadedFile['tmp_name'], $dest) && !move_uploaded_file($uploadedFile['tmp_name'], $dest)) {
// Déduplication : si ce fichier identique existe déjà dans un autre article, crée un hardlink
if (!file_exists($dest)) {
$existing = glob($this->dataDir . '/*/files/' . $name);
if (!empty($existing) && is_file($existing[0])) {
link($existing[0], $dest) || copy($existing[0], $dest);
@unlink($uploadedFile['tmp_name']);
return $name;
}
}
if (!file_exists($dest) && !rename($uploadedFile['tmp_name'], $dest) && !move_uploaded_file($uploadedFile['tmp_name'], $dest)) {
return null;
}
return $name;
+3
View File
@@ -163,6 +163,9 @@ $_layoutCurrentCat = trim($_GET['cat'] ?? '');
<?php if (isset($reactionStats)): ?>
<script src="<?= _av($_pub, 'js/reactions.js') ?>"></script>
<?php endif; ?>
<?php if (!empty($shareBar ?? false)): ?>
<script src="<?= _av($_pub, 'js/share.js') ?>" defer></script>
<?php endif; ?>
</body>
</html>
+23
View File
@@ -261,6 +261,28 @@ $hasSources = (!empty($externalLinks) || !empty($files))
</div>
</div>
<?php if ($article['published'] ?? false): ?>
<?php
$_shareUrl = rtrim(defined('APP_URL') ? APP_URL : '', '/') . '/post/' . rawurlencode($article['slug'] ?? '');
$_shareTitle = $article['title'] ?? '';
?>
<div class="d-flex flex-wrap align-items-center gap-2 my-3 py-2 border-top"
id="share-bar"
data-url="<?= htmlspecialchars($_shareUrl) ?>"
data-title="<?= htmlspecialchars($_shareTitle) ?>">
<span class="text-muted small me-1">Partager :</span>
<a href="mailto:?subject=<?= rawurlencode($_shareTitle) ?>&amp;body=<?= rawurlencode($_shareUrl) ?>"
class="btn btn-outline-secondary btn-sm" title="Par e-mail">✉ Mail</a>
<a href="https://x.com/intent/tweet?text=<?= rawurlencode($_shareTitle) ?>&amp;url=<?= rawurlencode($_shareUrl) ?>"
class="btn btn-outline-secondary btn-sm" target="_blank" rel="noopener noreferrer" title="X / Twitter">X</a>
<a href="https://www.linkedin.com/sharing/share-offsite/?url=<?= rawurlencode($_shareUrl) ?>"
class="btn btn-outline-secondary btn-sm" target="_blank" rel="noopener noreferrer" title="LinkedIn">in</a>
<a href="https://mastodon.social/share?text=<?= rawurlencode($_shareTitle . ' ' . $_shareUrl) ?>"
class="btn btn-outline-secondary btn-sm" target="_blank" rel="noopener noreferrer" title="Mastodon">🐘</a>
<button type="button" class="btn btn-outline-secondary btn-sm" id="share-copy">Copier le lien</button>
<button type="button" class="btn btn-outline-primary btn-sm" id="share-native" hidden>⬆ Partager</button>
</div>
<?php endif; ?>
<?php include __DIR__ . '/comments_section.php'; ?>
@@ -397,6 +419,7 @@ $hasSources = (!empty($externalLinks) || !empty($files))
<?php
$content = ob_get_clean();
$shareBar = (bool)($article['published'] ?? false);
$title = htmlspecialchars($article['title']);
$seoTitle = ($article['seo_title'] ?? '') ?: $article['title'];
$ogType = 'article';