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} + + + Read It Later + {if="$is_logged_in"} @@ -133,6 +137,10 @@ Bookmarklet detection logic NOTES + + + READ IT LATER +