diff --git a/shaarli-pro/css/style.css b/shaarli-pro/css/style.css
index d933ccd..ac331f0 100644
--- a/shaarli-pro/css/style.css
+++ b/shaarli-pro/css/style.css
@@ -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;
diff --git a/shaarli-pro/editlink.html b/shaarli-pro/editlink.html
index e77ee54..58d1b01 100644
--- a/shaarli-pro/editlink.html
+++ b/shaarli-pro/editlink.html
@@ -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}
diff --git a/shaarli-pro/js/script.js b/shaarli-pro/js/script.js
index 3d25b80..5dee8b9 100644
--- a/shaarli-pro/js/script.js
+++ b/shaarli-pro/js/script.js
@@ -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 = ' 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();
diff --git a/shaarli-pro/linklist.html b/shaarli-pro/linklist.html
index cbb1269..5d62c38 100644
--- a/shaarli-pro/linklist.html
+++ b/shaarli-pro/linklist.html
@@ -100,6 +100,9 @@
title="Supprimer" aria-label="Supprimer ce bookmark"
onclick="return confirm('Supprimer ce bookmark ?');">
+
{/if}
Notes
+
{if="$is_logged_in"}
@@ -133,6 +137,10 @@ Bookmarklet detection logic
NOTES
+