From 5d486d3d973abde114de0eb88de7cf1ed44b97b8 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Wed, 11 Feb 2026 14:22:16 -0500 Subject: [PATCH] feat: Implement a new tag cloud with an alphabet filter, including dedicated templates, styles, and scripts. --- shaarli-pro/css/style.css | 529 ++++++++++++++++++++++++++++++++--- shaarli-pro/js/script.js | 378 +++++++++++++++++++++++++ shaarli-pro/linklist.html | 16 +- shaarli-pro/page.footer.html | 29 +- shaarli-pro/tag.cloud.html | 15 +- 5 files changed, 912 insertions(+), 55 deletions(-) diff --git a/shaarli-pro/css/style.css b/shaarli-pro/css/style.css index 2698e50..9e908a5 100644 --- a/shaarli-pro/css/style.css +++ b/shaarli-pro/css/style.css @@ -28,7 +28,7 @@ --header-bg: #3b82f6; --header-text: #ffffff; - --sidebar-width: 260px; + --sidebar-width: 230px; --sidebar-collapsed: 60px; --header-height: 56px; @@ -100,6 +100,8 @@ html { scroll-behavior: smooth; + font-size: 14.5px; + /* Decrease global scale */ } body { @@ -1001,9 +1003,9 @@ input:checked+.theme-slider:before { /* ===== Content Container ===== */ .content-container { flex: 1; - padding: 1.5rem; - max-width: 1400px; - margin: 0 auto; + padding: 1rem 1.5rem; + max-width: 100%; + margin: 0; width: 100%; } @@ -1090,6 +1092,8 @@ input:checked+.theme-slider:before { transition: all 0.2s ease; position: relative; overflow: hidden; + content-visibility: auto; + contain-intrinsic-size: auto 300px; } /* Link card hover states - same for both public and private */ @@ -1137,7 +1141,8 @@ input:checked+.theme-slider:before { } .view-grid .link-select-checkbox { - right: 3.5rem; + right: auto; + left: 0.75rem; } [data-theme="dark"] .link-select-checkbox { @@ -1330,6 +1335,8 @@ input:checked+.theme-slider:before { .link-footer { display: flex; + flex-wrap: wrap; + gap: 0.5rem; align-items: center; justify-content: space-between; margin-top: 1rem; @@ -1348,6 +1355,8 @@ input:checked+.theme-slider:before { display: flex; flex-wrap: wrap; gap: 0.375rem; + flex: 1 1 auto; + min-width: 0; } .link-tag a { @@ -1375,6 +1384,11 @@ input:checked+.theme-slider:before { .link-actions { display: flex; gap: 0.25rem; + flex-shrink: 0; + align-items: center; + flex-wrap: nowrap; + margin-left: auto; + /* Force alignment to the right */ } .link-actions a, @@ -2375,6 +2389,10 @@ select:focus { box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); } +.picwall-pictureframe { + will-change: transform; +} + .picwall-pictureframe:hover { transform: translateY(-4px) scale(1.02); box-shadow: var(--shadow-xl); @@ -2578,6 +2596,8 @@ select:focus { margin-bottom: 0; box-shadow: none; transition: background 0.15s ease; + position: relative; + /* Ensure absolute positioning works */ } .view-compact .link-outer:first-child { @@ -2613,6 +2633,9 @@ select:focus { order: -1; margin-left: -1.5rem; margin-right: 0.5rem; + align-self: flex-start; + /* Move to top */ + margin-top: 0.125rem; } .view-compact .link-visibility-badge i { @@ -2674,6 +2697,8 @@ select:focus { display: flex; align-items: center; gap: 0.75rem; + width: 100%; + /* Force width to constrain tags */ } .view-compact .link-tag-list { @@ -3043,51 +3068,37 @@ select:focus { /* ===== Plugin Zone Styling ===== */ -/* Plugin buttons injected into paging (e.g., "Mark as Read") */ +/* Hide readitlater "Mark as Read" button from paging area */ .paging-plugin { - display: inline-flex; - align-items: center; + display: none; } -.paging-plugin a { - display: inline-flex; - align-items: center; - gap: 0.375rem; - padding: 0.375rem 0.75rem; - border-radius: 0.375rem; - font-size: 0.8rem; - font-weight: 500; - color: var(--text-secondary); - background: var(--bg-card); - border: 1px solid var(--border); - transition: all 0.2s ease; - text-decoration: none; - white-space: nowrap; -} - -.paging-plugin a:hover { - background: var(--primary-light); - color: var(--primary); - border-color: var(--primary); -} - -/* Plugin zone inside link cards */ +/* Plugin zone inside link cards - inline with actions */ .link-plugin { - display: flex; - align-items: center; - gap: 0.25rem; - margin-top: 0.25rem; + display: contents; } .link-plugin a { display: inline-flex; align-items: center; - gap: 0.25rem; - padding: 0.25rem 0.625rem; + justify-content: center; + width: 36px; + height: 36px; + background: transparent; + border: none; border-radius: 0.375rem; - font-size: 0.75rem; color: var(--text-muted); - transition: all 0.2s ease; + cursor: pointer; + transition: all 0.15s ease; + text-decoration: none; + padding: 0; + white-space: nowrap; + overflow: hidden; + font-size: 0; +} + +.link-plugin a i { + font-size: 1.15rem; } .link-plugin a:hover { @@ -3095,6 +3106,208 @@ select:focus { color: var(--primary); } +/* QR code plugin wrapper */ +.link-plugin .linkqrcode { + display: contents; +} + +.link-plugin a img { + width: 18px; + height: 18px; + opacity: 0.5; + transition: opacity 0.15s ease; + filter: var(--plugin-icon-filter, none); +} + +[data-theme="dark"] .link-plugin a img { + filter: invert(1) brightness(0.6); +} + +.link-plugin a:hover img { + opacity: 0.9; +} + +/* ===== ReadItLater Plugin Integration ===== */ +/* The readitlater toggle link */ +.link-plugin .readitlater-toggle { + display: inline-flex !important; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + font-size: 0 !important; + color: var(--text-muted); +} + +.link-plugin .readitlater-toggle:hover { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; +} + +.link-plugin .readitlater-toggle .readitlater-icon { + font-size: 0; + display: contents; +} + +.link-plugin .readitlater-toggle .readitlater-icon i { + font-size: 1.15rem; +} + +/* "To Read" badge on the card - grid/list views */ +.readitlater-badge { + position: absolute; + top: 0.75rem; + right: 3.25rem; + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.5rem; + background: rgba(239, 68, 68, 0.12); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 0.375rem; + font-size: 0.65rem; + font-weight: 700; + color: #ef4444; + z-index: 11; + white-space: nowrap; + text-transform: uppercase; + letter-spacing: 0.04em; + line-height: 1; +} + +.readitlater-badge i { + font-size: 0.8rem; + line-height: 1; +} + +[data-theme="dark"] .readitlater-badge { + background: rgba(239, 68, 68, 0.18); + border-color: rgba(239, 68, 68, 0.4); + color: #f87171; +} + +/* List view - badge next to visibility badge */ +.view-list .readitlater-badge { + right: 4rem; + top: 1.25rem; +} + +/* Compact view - badge absolute top-right */ +.view-compact .readitlater-badge { + position: absolute; + top: 0.5rem; + right: 0.5rem; + left: auto; + bottom: auto; + margin: 0; + align-self: auto; + order: unset; +} + +/* Red accent border for unread bookmarks */ +.link-outer.readitlater-unread { + border-left: 3px solid #ef4444; +} + +/* Unread eye icon color - red tint */ +.readitlater-unread .readitlater-toggle { + color: #ef4444 !important; +} + +.readitlater-unread .readitlater-toggle:hover { + background: rgba(239, 68, 68, 0.15) !important; +} + +/* Hide default QR code inline popup (we use our modal instead) */ +#permalinkQrcode { + display: none !important; +} + +/* QR Code Modal */ +.qrcode-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(4px); + z-index: 1100; + display: none; + align-items: center; + justify-content: center; + padding: 2rem; + opacity: 0; + transition: opacity 0.3s ease; +} + +.qrcode-modal-overlay.show { + display: flex; + opacity: 1; +} + +.qrcode-modal-content { + background: var(--bg-card); + padding: 2rem; + border-radius: 1rem; + box-shadow: var(--shadow-xl); + border: 1px solid var(--border); + position: relative; + max-width: 360px; + width: 100%; + text-align: center; + transform: translateY(20px) scale(0.95); + transition: transform 0.3s ease; +} + +.qrcode-modal-overlay.show .qrcode-modal-content { + transform: translateY(0) scale(1); +} + +.qrcode-modal-close { + position: absolute; + top: 0.75rem; + right: 0.75rem; + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--bg-body); + border: 1px solid var(--border); + color: var(--text-main); + font-size: 1.25rem; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; +} + +.qrcode-modal-close:hover { + background: var(--danger); + color: white; + border-color: var(--danger); + transform: rotate(90deg); +} + +.qrcode-modal-content img { + display: block; + max-width: 280px; + width: 100%; + height: auto; + margin: 0 auto; + border-radius: 0.75rem; + background: white; + padding: 1.25rem; + box-sizing: border-box; +} + +.qrcode-modal-title { + margin-top: 1rem; + font-size: 0.85rem; + color: var(--text-secondary); + word-break: break-all; +} + /* Single page pagination - centered stats only */ .paging.single-page { justify-content: center; @@ -3217,4 +3430,242 @@ select:focus { /* Button style adjustment if needed */ .view-desc-btn { cursor: pointer; +} + +/* ===== Persistent Media Player ===== */ +.media-player-bar { + position: fixed; + bottom: 0; + left: var(--sidebar-width); + right: 0; + background: var(--bg-card); + border-top: 1px solid var(--border); + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15); + z-index: 200; + transform: translateY(100%); + transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1); + backdrop-filter: blur(12px); +} + +.media-player-bar.show { + transform: translateY(0); +} + +.media-player-inner { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 1.25rem; + max-width: 1400px; + margin: 0 auto; +} + +/* Play/Pause Button */ +.media-player-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--primary); + color: white; + border: none; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.media-player-btn:hover { + background: var(--primary-hover); + transform: scale(1.05); +} + +.media-player-btn i { + font-size: 1.25rem; +} + +.media-player-btn-sm { + width: 32px; + height: 32px; + background: transparent; + color: var(--text-muted); + border-radius: 0.375rem; +} + +.media-player-btn-sm:hover { + background: var(--primary-light); + color: var(--primary); + transform: none; +} + +.media-player-btn-sm i { + font-size: 1.1rem; +} + +/* Info Section */ +.media-player-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.media-player-title { + font-size: 0.85rem; + font-weight: 500; + color: var(--text-main); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 400px; +} + +/* Progress Bar */ +.media-player-progress-wrap { + width: 100%; +} + +.media-player-progress { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 4px; + background: var(--border); + border-radius: 2px; + outline: none; + cursor: pointer; + transition: height 0.15s ease; +} + +.media-player-progress:hover { + height: 6px; +} + +.media-player-progress::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--primary); + cursor: pointer; + border: 2px solid white; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); +} + +.media-player-progress::-moz-range-thumb { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--primary); + cursor: pointer; + border: 2px solid white; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); +} + +/* Time Display */ +.media-player-time { + font-size: 0.75rem; + color: var(--text-muted); + white-space: nowrap; + flex-shrink: 0; + font-variant-numeric: tabular-nums; + min-width: 90px; + text-align: center; +} + +/* Volume */ +.media-player-volume-wrap { + display: flex; + align-items: center; + gap: 0.25rem; + flex-shrink: 0; +} + +.media-player-volume { + -webkit-appearance: none; + appearance: none; + width: 80px; + height: 4px; + background: var(--border); + border-radius: 2px; + outline: none; + cursor: pointer; +} + +.media-player-volume::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--primary); + cursor: pointer; +} + +.media-player-volume::-moz-range-thumb { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--primary); + cursor: pointer; +} + +/* Close Button */ +.media-player-close:hover { + background: rgba(239, 68, 68, 0.1) !important; + color: var(--danger) !important; +} + +/* Play button injected into bookmark action bars */ +.media-play-action { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background: transparent; + border: none; + border-radius: 0.375rem; + color: var(--primary); + cursor: pointer; + transition: all 0.15s ease; + text-decoration: none; +} + +.media-play-action:hover { + background: var(--primary-light); + color: var(--primary-hover); +} + +.media-play-action i { + font-size: 1.15rem; +} + +/* Responsive adjustments for media player */ +@media (max-width: 1024px) { + .media-player-bar { + left: 0; + } +} + +@media (max-width: 768px) { + .media-player-inner { + gap: 0.5rem; + padding: 0.5rem 1rem; + } + + .media-player-title { + max-width: 150px; + } + + .media-player-volume-wrap { + display: none; + } + + .media-player-time { + display: none; + } } \ No newline at end of file diff --git a/shaarli-pro/js/script.js b/shaarli-pro/js/script.js index df65355..fe76c00 100644 --- a/shaarli-pro/js/script.js +++ b/shaarli-pro/js/script.js @@ -995,4 +995,382 @@ document.addEventListener('DOMContentLoaded', () => { } } }); + + // ===== QR Code Plugin Modal ===== + const qrcodeModal = document.getElementById('qrcode-modal'); + const qrcodeModalBody = document.getElementById('qrcode-modal-body'); + const qrcodeModalClose = document.getElementById('qrcode-modal-close'); + + function openQrcodeModal(permalink, title) { + if (!qrcodeModal || !qrcodeModalBody) return; + + // Show loading state + qrcodeModalBody.innerHTML = ` +
Generating QR Code...
+ `; + qrcodeModal.classList.add('show'); + document.body.style.overflow = 'hidden'; + + // Generate QR code using the qr.js library (loaded by the Shaarli qrcode plugin) + function renderQR() { + if (typeof qr !== 'undefined') { + const image = qr.image({ size: 8, value: permalink }); + if (image) { + qrcodeModalBody.innerHTML = ''; + image.style.maxWidth = '100%'; + image.style.borderRadius = '0.5rem'; + image.style.background = 'white'; + image.style.padding = '0.75rem'; + qrcodeModalBody.appendChild(image); + if (title) { + const titleDiv = document.createElement('div'); + titleDiv.className = 'qrcode-modal-title'; + titleDiv.textContent = title; + qrcodeModalBody.appendChild(titleDiv); + } + } else { + qrcodeModalBody.innerHTML = `
Failed to generate QR Code
`; + } + } else { + // qr.js library not yet loaded — load it dynamically + const basePath = document.querySelector('input[name="js_base_path"]')?.value || ''; + const script = document.createElement('script'); + script.src = basePath + '/plugins/qrcode/qr-1.1.3.min.js'; + document.body.appendChild(script); + setTimeout(() => renderQR(), 300); + } + } + renderQR(); + } + + function closeQrcodeModal() { + if (qrcodeModal) { + qrcodeModal.classList.remove('show'); + document.body.style.overflow = ''; + } + } + + qrcodeModalClose?.addEventListener('click', closeQrcodeModal); + qrcodeModal?.addEventListener('click', (e) => { + if (e.target === qrcodeModal) closeQrcodeModal(); + }); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && qrcodeModal?.classList.contains('show')) closeQrcodeModal(); + }); + + // ===== Transform QR code plugin icons ===== + document.querySelectorAll('.link-plugin .linkqrcode, .link-plugin img.qrcode').forEach(el => { + // The QR code plugin injects:
+ const img = el.tagName === 'IMG' ? el : el.querySelector('img.qrcode'); + if (!img) return; + + const permalink = img.dataset.permalink || ''; + const parentLink = img.closest('a') || img.parentElement; + + // Replace img with MDI icon + const icon = document.createElement('i'); + icon.className = 'mdi mdi-qrcode'; + + if (parentLink.tagName === 'A' || parentLink.classList.contains('linkqrcode')) { + // Wrap in a clickable element if not already + const btn = document.createElement('a'); + btn.href = '#'; + btn.className = 'qrcode-trigger'; + btn.title = 'QR Code'; + btn.dataset.permalink = permalink; + btn.appendChild(icon); + + // Replace the whole linkqrcode div or img with our button + const wrapper = el.classList.contains('linkqrcode') ? el : el.closest('.linkqrcode') || el; + wrapper.replaceWith(btn); + } + }); + + // Click handler for QR code icons + document.addEventListener('click', (e) => { + const trigger = e.target.closest('.qrcode-trigger, .link-plugin img.qrcode'); + if (!trigger) return; + + e.preventDefault(); + e.stopPropagation(); + + const permalink = trigger.dataset?.permalink || + trigger.querySelector('img')?.dataset?.permalink || ''; + const card = trigger.closest('.link-outer'); + const title = card?.querySelector('.link-title')?.textContent?.trim() || permalink; + + if (permalink) { + openQrcodeModal(permalink, title); + } + }); + + // ===== ReadItLater Plugin Integration ===== + document.querySelectorAll('.link-plugin .readitlater-toggle').forEach(toggle => { + const iconSpan = toggle.querySelector('.readitlater-icon'); + if (!iconSpan) return; + + const card = toggle.closest('.link-outer'); + const isUnread = card?.classList.contains('readitlater-unread'); + const titleText = toggle.getAttribute('title') || ''; + + // 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'; + } + iconSpan.innerHTML = ''; + iconSpan.appendChild(mdiIcon); + + // Set proper tooltip + toggle.setAttribute('title', titleText || (isUnread ? 'Mark as Read' : 'Read it later')); + + // 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'; + card.appendChild(badge); + } + }); + + // ===== Persistent Media Player ===== + const MEDIA_EXTENSIONS = [ + '.mp3', '.mp4', '.ogg', '.webm', '.m3u8', '.flac', '.wav', '.aac', + '.m4a', '.opus', '.wma', '.oga', '.m3u', '.pls' + ]; + const STREAM_PATTERNS = [ + 'icecast', 'shoutcast', 'stream', 'radio', 'listen', 'audio', + '/live', '.stream' + ]; + + function isMediaUrl(url) { + if (!url) return false; + const lower = url.toLowerCase(); + // Check file extensions + for (const ext of MEDIA_EXTENSIONS) { + if (lower.includes(ext)) return true; + } + // Check streaming patterns + for (const pattern of STREAM_PATTERNS) { + if (lower.includes(pattern) && (lower.includes('http') || lower.includes('//'))) { + // Must also look like an audio/stream URL (not just any page about audio) + if (lower.endsWith('.mp3') || lower.endsWith('.ogg') || lower.endsWith('.m3u8') || + lower.endsWith('.aac') || lower.endsWith('.flac') || lower.endsWith('.m4a') || + lower.endsWith('.wav') || lower.endsWith('.opus') || lower.endsWith('.pls') || + lower.endsWith('.m3u') || lower.includes('.mp3') || lower.includes('.ogg') || + lower.includes('stream') || lower.includes('icecast') || lower.includes('listen')) { + return true; + } + } + } + return false; + } + + const playerBar = document.getElementById('media-player-bar'); + const playerAudio = document.getElementById('media-player-audio'); + const playerPlayBtn = document.getElementById('media-player-play'); + const playerPlayIcon = document.getElementById('media-player-play-icon'); + const playerTitle = document.getElementById('media-player-title'); + const playerProgress = document.getElementById('media-player-progress'); + const playerTime = document.getElementById('media-player-time'); + const playerVolume = document.getElementById('media-player-volume'); + const playerVolBtn = document.getElementById('media-player-vol-btn'); + const playerVolIcon = document.getElementById('media-player-vol-icon'); + const playerCloseBtn = document.getElementById('media-player-close'); + + function formatTime(seconds) { + if (!seconds || isNaN(seconds) || !isFinite(seconds)) return '0:00'; + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return m + ':' + (s < 10 ? '0' : '') + s; + } + + function showPlayer(url, title) { + if (!playerBar || !playerAudio) return; + + playerAudio.src = url; + playerAudio.volume = parseFloat(playerVolume?.value || 0.8); + playerAudio.play().catch(() => { }); + + if (playerTitle) playerTitle.textContent = title || url; + if (playerPlayIcon) { + playerPlayIcon.className = 'mdi mdi-pause'; + } + playerBar.classList.add('show'); + + // Save to localStorage for persistence + localStorage.setItem('mediaPlayerUrl', url); + localStorage.setItem('mediaPlayerTitle', title || url); + localStorage.setItem('mediaPlayerPlaying', 'true'); + } + + function closePlayer() { + if (playerAudio) { + playerAudio.pause(); + playerAudio.src = ''; + } + if (playerBar) playerBar.classList.remove('show'); + localStorage.removeItem('mediaPlayerUrl'); + localStorage.removeItem('mediaPlayerTitle'); + localStorage.removeItem('mediaPlayerPosition'); + localStorage.removeItem('mediaPlayerPlaying'); + } + + function togglePlayPause() { + if (!playerAudio) return; + if (playerAudio.paused) { + playerAudio.play().catch(() => { }); + } else { + playerAudio.pause(); + } + } + + if (playerAudio) { + playerAudio.addEventListener('play', () => { + if (playerPlayIcon) playerPlayIcon.className = 'mdi mdi-pause'; + localStorage.setItem('mediaPlayerPlaying', 'true'); + }); + + playerAudio.addEventListener('pause', () => { + if (playerPlayIcon) playerPlayIcon.className = 'mdi mdi-play'; + localStorage.setItem('mediaPlayerPlaying', 'false'); + }); + + playerAudio.addEventListener('timeupdate', () => { + if (!playerAudio.duration) return; + const pct = (playerAudio.currentTime / playerAudio.duration) * 100; + if (playerProgress) playerProgress.value = pct; + if (playerTime) { + playerTime.textContent = formatTime(playerAudio.currentTime) + ' / ' + formatTime(playerAudio.duration); + } + // Persist position every 2 seconds + if (Math.floor(playerAudio.currentTime) % 2 === 0) { + localStorage.setItem('mediaPlayerPosition', playerAudio.currentTime); + } + }); + + playerAudio.addEventListener('ended', () => { + if (playerPlayIcon) playerPlayIcon.className = 'mdi mdi-play'; + if (playerProgress) playerProgress.value = 0; + localStorage.setItem('mediaPlayerPlaying', 'false'); + }); + + playerAudio.addEventListener('loadedmetadata', () => { + // For streams with infinite duration, hide the time display + if (!isFinite(playerAudio.duration)) { + if (playerTime) playerTime.textContent = 'LIVE'; + if (playerProgress) playerProgress.style.display = 'none'; + } else { + if (playerProgress) playerProgress.style.display = ''; + } + }); + } + + // Player controls + playerPlayBtn?.addEventListener('click', togglePlayPause); + playerCloseBtn?.addEventListener('click', closePlayer); + + playerProgress?.addEventListener('input', () => { + if (playerAudio && playerAudio.duration && isFinite(playerAudio.duration)) { + playerAudio.currentTime = (playerProgress.value / 100) * playerAudio.duration; + } + }); + + playerVolume?.addEventListener('input', () => { + if (playerAudio) { + playerAudio.volume = playerVolume.value; + updateVolIcon(playerVolume.value); + } + }); + + playerVolBtn?.addEventListener('click', () => { + if (!playerAudio) return; + if (playerAudio.volume > 0) { + playerAudio.dataset.prevVol = playerAudio.volume; + playerAudio.volume = 0; + if (playerVolume) playerVolume.value = 0; + } else { + const prev = parseFloat(playerAudio.dataset.prevVol || 0.8); + playerAudio.volume = prev; + if (playerVolume) playerVolume.value = prev; + } + updateVolIcon(playerAudio.volume); + }); + + function updateVolIcon(vol) { + if (!playerVolIcon) return; + if (vol <= 0) { + playerVolIcon.className = 'mdi mdi-volume-off'; + } else if (vol < 0.5) { + playerVolIcon.className = 'mdi mdi-volume-medium'; + } else { + playerVolIcon.className = 'mdi mdi-volume-high'; + } + } + + // --- Inject play buttons into bookmark cards with media URLs --- + document.querySelectorAll('.link-outer').forEach(card => { + const urlEl = card.querySelector('.link-url'); + const titleEl = card.querySelector('.link-title'); + if (!urlEl) return; + + const url = urlEl.textContent.trim(); + const realUrl = titleEl?.getAttribute('href') || url; + + if (isMediaUrl(url) || isMediaUrl(realUrl)) { + const actionsDiv = card.querySelector('.link-actions'); + if (!actionsDiv) return; + + const playBtn = document.createElement('button'); + playBtn.className = 'media-play-action'; + playBtn.title = 'Play media'; + playBtn.innerHTML = ''; + + // Insert before the "Open Link" button (last child usually) + const openLinkBtn = actionsDiv.querySelector('a[title="Open Link"]'); + if (openLinkBtn) { + actionsDiv.insertBefore(playBtn, openLinkBtn); + } else { + actionsDiv.appendChild(playBtn); + } + + playBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + const mediaUrl = isMediaUrl(url) ? url : realUrl; + const mediaTitle = titleEl?.textContent?.trim() || url; + showPlayer(mediaUrl, mediaTitle); + }); + } + }); + + // --- Restore player on page load --- + (function restorePlayer() { + const savedUrl = localStorage.getItem('mediaPlayerUrl'); + const savedTitle = localStorage.getItem('mediaPlayerTitle'); + const savedPosition = parseFloat(localStorage.getItem('mediaPlayerPosition') || 0); + const wasPlaying = localStorage.getItem('mediaPlayerPlaying') === 'true'; + + if (savedUrl && playerBar && playerAudio) { + playerAudio.src = savedUrl; + if (playerTitle) playerTitle.textContent = savedTitle || savedUrl; + playerBar.classList.add('show'); + + playerAudio.addEventListener('loadedmetadata', function onMeta() { + if (savedPosition > 0 && isFinite(playerAudio.duration)) { + playerAudio.currentTime = savedPosition; + } + if (wasPlaying) { + playerAudio.play().catch(() => { }); + } + playerAudio.removeEventListener('loadedmetadata', onMeta); + }, { once: true }); + + // Set volume from saved state + const savedVol = playerVolume?.value || 0.8; + playerAudio.volume = parseFloat(savedVol); + } + })(); }); diff --git a/shaarli-pro/linklist.html b/shaarli-pro/linklist.html index e912194..1966a94 100644 --- a/shaarli-pro/linklist.html +++ b/shaarli-pro/linklist.html @@ -48,7 +48,7 @@ {if="$is_logged_in"} {/if} -{if="$value.thumbnail !== false"}{/if} +{if="$value.thumbnail !== false"}{/if} @@ -100,6 +100,14 @@ + +
+
+ +
+
+
+ {include="page.footer"} diff --git a/shaarli-pro/page.footer.html b/shaarli-pro/page.footer.html index 202ce27..22eafe2 100644 --- a/shaarli-pro/page.footer.html +++ b/shaarli-pro/page.footer.html @@ -21,8 +21,7 @@ {/loop} - - +
@@ -35,4 +34,30 @@
+ + +
+ +
+ +
+
No media
+
+ +
+
+
0:00 / 0:00
+
+ + +
+ +
+
\ No newline at end of file diff --git a/shaarli-pro/tag.cloud.html b/shaarli-pro/tag.cloud.html index ae9a009..051372c 100644 --- a/shaarli-pro/tag.cloud.html +++ b/shaarli-pro/tag.cloud.html @@ -84,27 +84,22 @@ /* Alphabet Filter Bar - Professional Design */ .alphabet-filter { display: flex; - flex-wrap: nowrap; + flex-wrap: wrap; justify-content: center; align-items: center; - gap: 0; + gap: 0.25rem; margin-bottom: 1.5rem; - padding: 0.35rem; + padding: 0.75rem; background: linear-gradient(135deg, rgba(30, 41, 59, 0.8) 0%, rgba(15, 23, 42, 0.9) 100%); - border-radius: 50px; + border-radius: 1rem; border: 1px solid rgba(99, 102, 241, 0.2); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px); - overflow-x: auto; - scrollbar-width: none; - -ms-overflow-style: none; } - .alphabet-filter::-webkit-scrollbar { - display: none; - } + .alpha-btn { position: relative;