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:
2026-02-18 16:41:49 -05:00
parent 6b1f4b3e28
commit ab097cd1fe
5 changed files with 268 additions and 26 deletions
+197 -25
View File
@@ -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();