feat: implémenter système Read It Later natif basé sur tags avec bouton toggle interactif, badge visuel rouge "Read Later", synchronisation bidirectionnelle des tags (readitlater/readlater/toread), intégration dans sidebar et header de navigation, mise à jour automatique des pills de tags, et support complet thème clair/sombre avec états hover et loading
This commit is contained in:
parent
6b1f4b3e28
commit
ab097cd1fe
@ -1233,6 +1233,35 @@ input:checked+.theme-slider:before {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link-actions .readitlater-toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link-actions .readitlater-toggle-btn i {
|
||||
font-size: 1.15rem;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.link-actions .readitlater-toggle-btn.active {
|
||||
color: #dc2626;
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
}
|
||||
|
||||
.link-actions .readitlater-toggle-btn.is-loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.link-hover-btn:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
@ -1289,6 +1318,31 @@ input:checked+.theme-slider:before {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.link-readlater-badge {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 3.2rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
padding: 0 0.6rem;
|
||||
border-radius: 0.5rem;
|
||||
background: rgba(239, 68, 68, 0.92);
|
||||
color: #fff;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.35);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .link-readlater-badge {
|
||||
background: rgba(248, 113, 113, 0.9);
|
||||
color: #1b0b0b;
|
||||
}
|
||||
|
||||
.link-visibility-badge i {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
@ -1487,6 +1541,11 @@ input:checked+.theme-slider:before {
|
||||
right: 1.5rem;
|
||||
}
|
||||
|
||||
.view-list .link-readlater-badge {
|
||||
top: 1.25rem;
|
||||
right: 3.95rem;
|
||||
}
|
||||
|
||||
/* List view - selection */
|
||||
.view-list .link-select-checkbox {
|
||||
left: 1rem;
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
{$index=""}
|
||||
{/if}
|
||||
{$batchModeValue=empty($batch_mode) ? '0' : '1'}
|
||||
{function="($readLaterChecked = strpos(' ' . $link.tags . ' ', ' readlater ') != false || strpos(' ' . $link.tags . ' ', ' toread ') != false) ? '' : ''"}
|
||||
{function="($readLaterChecked = strpos(' ' . $link.tags . ' ', ' readitlater ') != false || strpos(' ' . $link.tags . ' ', ' readlater ') != false || strpos(' ' . $link.tags . ' ', ' toread ') != false) ? '' : ''"}
|
||||
{function="($noteDefaultChecked = $link_is_new && empty($link.url)) ? '' : ''"}
|
||||
{function="($noteChecked = strpos(' ' . $link.tags . ' ', ' note ') != false || $noteDefaultChecked) ? '' : ''"}
|
||||
{$effectiveTags=$link.tags}
|
||||
|
||||
@ -1123,34 +1123,206 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== ReadItLater Plugin Integration =====
|
||||
document.querySelectorAll('.link-plugin .readitlater-toggle').forEach(toggle => {
|
||||
const iconSpan = toggle.querySelector('.readitlater-icon');
|
||||
if (!iconSpan) return;
|
||||
// ===== Read It Later (tag-based, no plugin dependency) =====
|
||||
const READ_IT_LATER_TAG = 'readitlater';
|
||||
const READ_IT_LATER_ALIASES = ['readitlater', 'readlater', 'toread'];
|
||||
|
||||
const card = toggle.closest('.link-outer');
|
||||
const isUnread = card?.classList.contains('readitlater-unread');
|
||||
const titleText = toggle.getAttribute('title') || '';
|
||||
const normalizeTagValue = (tagValue) => (tagValue || '').trim().toLowerCase();
|
||||
|
||||
// Replace text content with MDI icon
|
||||
const mdiIcon = document.createElement('i');
|
||||
if (isUnread) {
|
||||
mdiIcon.className = 'mdi mdi-eye-off';
|
||||
} else {
|
||||
mdiIcon.className = 'mdi mdi-eye-outline';
|
||||
const isReadItLaterTag = (tagValue) => READ_IT_LATER_ALIASES.includes(normalizeTagValue(tagValue));
|
||||
|
||||
function getReadItLaterEditUrl(card) {
|
||||
if (!card) return '';
|
||||
|
||||
const id = card.dataset.id;
|
||||
if (id) {
|
||||
return `${shaarli.basePath}/admin/shaare/${encodeURIComponent(id)}`;
|
||||
}
|
||||
iconSpan.innerHTML = '';
|
||||
iconSpan.appendChild(mdiIcon);
|
||||
|
||||
// Set proper tooltip
|
||||
toggle.setAttribute('title', titleText || (isUnread ? 'Mark as Read' : 'Read it later'));
|
||||
const editLink = card.querySelector('.link-actions a[href*="/admin/shaare/"]:not([href*="/pin"]):not([href*="/delete"])');
|
||||
return editLink ? editLink.href : '';
|
||||
}
|
||||
|
||||
// Add "To Read" badge to unread cards
|
||||
if (isUnread && card && !card.querySelector('.readitlater-badge')) {
|
||||
const badge = document.createElement('div');
|
||||
badge.className = 'readitlater-badge';
|
||||
badge.innerHTML = '<i class="mdi mdi-bookmark-check"></i> To Read';
|
||||
function collectBookmarkFormData(form) {
|
||||
const formData = new URLSearchParams();
|
||||
const inputs = form.querySelectorAll('input, textarea, select');
|
||||
inputs.forEach((input) => {
|
||||
if (!input.name || input.disabled) return;
|
||||
|
||||
if (input.type === 'checkbox') {
|
||||
if (input.checked) {
|
||||
formData.append(input.name, input.value || 'on');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.type === 'radio' && !input.checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append(input.name, input.value || '');
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
async function updateReadItLaterTag(editUrl, enableTag) {
|
||||
const editResponse = await fetch(editUrl, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
if (!editResponse.ok) {
|
||||
throw new Error('Unable to load bookmark edit form.');
|
||||
}
|
||||
|
||||
const html = await editResponse.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const form = doc.querySelector('form[name="linkform"]');
|
||||
if (!form) {
|
||||
throw new Error('Bookmark edit form not found.');
|
||||
}
|
||||
|
||||
const formData = collectBookmarkFormData(form);
|
||||
const existingTags = (formData.get('lf_tags') || '')
|
||||
.split(/[\s,]+/)
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
.filter((tag) => !isReadItLaterTag(tag));
|
||||
|
||||
if (enableTag) {
|
||||
existingTags.push(READ_IT_LATER_TAG);
|
||||
}
|
||||
|
||||
formData.set('lf_tags', existingTags.join(' '));
|
||||
formData.set('save_edit', '1');
|
||||
|
||||
const submitResponse = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData.toString(),
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
if (!submitResponse.ok) {
|
||||
throw new Error('Unable to save bookmark tags.');
|
||||
}
|
||||
}
|
||||
|
||||
function syncReadItLaterTagPill(card, isActive) {
|
||||
const tagList = card.querySelector('.link-tag-list');
|
||||
if (!tagList) return;
|
||||
|
||||
const existingPill = Array.from(tagList.querySelectorAll('.link-tag')).find((tagEl) => {
|
||||
const rawTag = tagEl.dataset.tag || tagEl.querySelector('.link-tag-link')?.textContent || tagEl.textContent || '';
|
||||
return normalizeTagValue(rawTag) === READ_IT_LATER_TAG;
|
||||
});
|
||||
|
||||
if (!isActive) {
|
||||
existingPill?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingPill) return;
|
||||
|
||||
const pill = document.createElement('span');
|
||||
pill.className = 'link-tag';
|
||||
pill.dataset.tag = READ_IT_LATER_TAG;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.className = 'link-tag-link';
|
||||
link.href = `${shaarli.basePath}/add-tag/${encodeURIComponent(READ_IT_LATER_TAG)}`;
|
||||
link.textContent = READ_IT_LATER_TAG;
|
||||
pill.appendChild(link);
|
||||
|
||||
if (shaarli.isAuth) {
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'tag-remove-btn';
|
||||
removeBtn.dataset.tag = READ_IT_LATER_TAG;
|
||||
removeBtn.setAttribute('aria-label', 'Supprimer le tag readitlater');
|
||||
removeBtn.title = 'Supprimer';
|
||||
removeBtn.textContent = '×';
|
||||
pill.appendChild(removeBtn);
|
||||
}
|
||||
|
||||
tagList.appendChild(pill);
|
||||
}
|
||||
|
||||
function syncReadItLaterCardUI(card, isActive) {
|
||||
if (!card) return;
|
||||
|
||||
card.classList.toggle('readitlater-active', isActive);
|
||||
|
||||
let badge = card.querySelector('.link-readlater-badge');
|
||||
if (isActive && !badge) {
|
||||
badge = document.createElement('div');
|
||||
badge.className = 'link-readlater-badge';
|
||||
badge.textContent = 'Read Later';
|
||||
card.appendChild(badge);
|
||||
} else if (!isActive && badge) {
|
||||
badge.remove();
|
||||
}
|
||||
|
||||
const toggleBtn = card.querySelector('.readitlater-toggle-btn');
|
||||
if (toggleBtn) {
|
||||
const titleText = isActive ? 'Retirer de Read It Later' : 'Ajouter a Read It Later';
|
||||
toggleBtn.classList.toggle('active', isActive);
|
||||
toggleBtn.dataset.active = isActive ? '1' : '0';
|
||||
toggleBtn.setAttribute('title', titleText);
|
||||
toggleBtn.setAttribute('aria-label', titleText);
|
||||
|
||||
const icon = toggleBtn.querySelector('i');
|
||||
if (icon) {
|
||||
icon.className = `mdi ${isActive ? 'mdi-bookmark-clock' : 'mdi-bookmark-clock-outline'}`;
|
||||
}
|
||||
}
|
||||
|
||||
syncReadItLaterTagPill(card, isActive);
|
||||
}
|
||||
|
||||
document.querySelectorAll('.link-outer').forEach((card) => {
|
||||
const cardTags = Array.from(card.querySelectorAll('.link-tag')).map((tagEl) => {
|
||||
const rawTag = tagEl.dataset.tag || tagEl.querySelector('.link-tag-link')?.textContent || tagEl.textContent || '';
|
||||
return normalizeTagValue(rawTag);
|
||||
});
|
||||
const isActive = cardTags.some(isReadItLaterTag) || card.classList.contains('readitlater-active') || card.classList.contains('readitlater-unread');
|
||||
syncReadItLaterCardUI(card, isActive);
|
||||
});
|
||||
|
||||
document.addEventListener('click', async (event) => {
|
||||
const toggleBtn = event.target.closest('.readitlater-toggle-btn');
|
||||
if (!toggleBtn) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const card = toggleBtn.closest('.link-outer');
|
||||
if (!card) return;
|
||||
|
||||
const editUrl = getReadItLaterEditUrl(card);
|
||||
if (!editUrl) return;
|
||||
|
||||
const isActive = card.classList.contains('readitlater-active') || toggleBtn.dataset.active === '1';
|
||||
const nextState = !isActive;
|
||||
|
||||
if (toggleBtn.disabled) return;
|
||||
|
||||
toggleBtn.disabled = true;
|
||||
toggleBtn.classList.add('is-loading');
|
||||
|
||||
try {
|
||||
await updateReadItLaterTag(editUrl, nextState);
|
||||
syncReadItLaterCardUI(card, nextState);
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle readitlater tag:', error);
|
||||
alert('Impossible de mettre a jour Read It Later pour ce bookmark.');
|
||||
} finally {
|
||||
toggleBtn.disabled = false;
|
||||
toggleBtn.classList.remove('is-loading');
|
||||
}
|
||||
});
|
||||
|
||||
@ -1252,7 +1424,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const syncReadLaterCheckbox = () => {
|
||||
if (!readLaterCheckbox) return;
|
||||
readLaterCheckbox.checked = tags.some((tag) => /^(readlater|toread)$/i.test(tag));
|
||||
readLaterCheckbox.checked = tags.some((tag) => /^(readitlater|readlater|toread)$/i.test(tag));
|
||||
};
|
||||
|
||||
const syncNoteCheckbox = () => {
|
||||
@ -1355,10 +1527,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (readLaterCheckbox) {
|
||||
readLaterCheckbox.addEventListener('change', () => {
|
||||
tags = tags.filter((tag) => !/^(readlater|toread)$/i.test(tag));
|
||||
tags = tags.filter((tag) => !/^(readitlater|readlater|toread)$/i.test(tag));
|
||||
|
||||
if (readLaterCheckbox.checked) {
|
||||
tags.push('readlater');
|
||||
tags.push('readitlater');
|
||||
}
|
||||
|
||||
updateHiddenTags();
|
||||
|
||||
@ -100,6 +100,9 @@
|
||||
title="Supprimer" aria-label="Supprimer ce bookmark"
|
||||
onclick="return confirm('Supprimer ce bookmark ?');"><i
|
||||
class="mdi mdi-delete" aria-hidden="true"></i></a>
|
||||
<button type="button" class="readitlater-toggle-btn" data-tag="readitlater" data-active="0" title="Ajouter à Read It Later" aria-label="Ajouter à Read It Later">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></svg>
|
||||
</button>
|
||||
{/if}
|
||||
<!-- fullscreen button (in all 3 views) -->
|
||||
<a href="#" class="view-desc-btn" data-id="{$value.id}"
|
||||
|
||||
@ -48,6 +48,10 @@ Bookmarklet detection logic
|
||||
<i class="mdi mdi-note-text-outline" aria-hidden="true"></i>
|
||||
<span>Notes</span>
|
||||
</a>
|
||||
<a href="{$base_path}/?searchtags=readitlater" class="sidebar-link{if="isset($search_tags)&&$search_tags=='readitlater'"} active{/if}" aria-label="Read It Later">
|
||||
<i class="mdi mdi-bookmark-outline" aria-hidden="true"></i>
|
||||
<span>Read It Later</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{if="$is_logged_in"}
|
||||
@ -133,6 +137,10 @@ Bookmarklet detection logic
|
||||
<i class="mdi mdi-note-text-outline" aria-hidden="true"></i>
|
||||
<span>NOTES</span>
|
||||
</a>
|
||||
<a href="{$base_path}/?searchtags=readitlater" class="header-nav-link{if="isset($search_tags)&&$search_tags=='readitlater'"} active{/if}" aria-label="Read It Later">
|
||||
<i class="mdi mdi-bookmark-outline" aria-hidden="true"></i>
|
||||
<span>READ IT LATER</span>
|
||||
</a>
|
||||
<button class="header-nav-link" id="search-toggle-btn" aria-label="Rechercher (raccourci S)">
|
||||
<i class="mdi mdi-magnify" aria-hidden="true"></i>
|
||||
<span>RECHERCHE</span>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user