diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb5d15..bd4a625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ All notable changes to this project will be documented in this file. - No unreleased changes recorded yet. +## 1.5.6 - 2026-07-25 + +### Changed + +- Playlist slide picker now uses a button to reveal already added slides, while still defaulting to showing only available slides. +- Already-added slides in the picker are now labeled and visually muted to make them easier to spot. + +### Fixed + +- Thumbnails in slide and playlist tables now follow the slide canvas ratio and cap at 5.5rem tall. +- Thumbnail-only table columns are no longer sortable. + ## 1.5.5 - 2026-07-25 ### Fixed diff --git a/package.json b/package.json index 208922e..52c418d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "1.5.5", + "version": "1.5.6", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/web/public/css/theme-custom.css b/src/web/public/css/theme-custom.css index ba694f8..b494a90 100644 --- a/src/web/public/css/theme-custom.css +++ b/src/web/public/css/theme-custom.css @@ -1265,6 +1265,19 @@ html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid__tile { box-shadow: none; } +.playlist-slide-picker-card.is-assigned { + border-color: rgba(var(--bs-primary-rgb), 0.35); + background: rgba(var(--bs-primary-rgb), 0.08); +} + +.playlist-slide-picker-card.is-assigned .playlist-slide-picker-media { + filter: saturate(0.85) grayscale(0.25); +} + +.playlist-slide-picker-card.is-assigned .playlist-slide-picker-title { + color: var(--bs-body-secondary); +} + .playlist-slide-picker-card.is-hidden { display: none !important; } @@ -1322,6 +1335,10 @@ html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid__tile { padding: 0 0.9rem 0.6rem; } +.playlist-slide-picker-filter-button { + white-space: nowrap; +} + .playlist-slide-picker-meta { display: none; } @@ -1335,7 +1352,7 @@ html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid__tile { opacity: 0; background: rgba(var(--bs-body-bg-rgb), 0.88); border-radius: 999px; - padding: 0.2rem 0.3rem; + padding: 0.2rem 0.45rem; box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.12); } @@ -1345,6 +1362,17 @@ html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid__tile { .playlist-slide-picker-badge { display: none; + position: absolute; + left: 0.5rem; + top: 0.5rem; + z-index: 1; + font-size: 0.65rem; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.playlist-slide-picker-card.is-assigned .playlist-slide-picker-badge { + display: inline-flex; } .playlist-slide-picker-empty.is-hidden { @@ -1438,8 +1466,9 @@ table.table > :not(caption) > * > * { .playlist-slide-thumb { flex: 0 0 auto; - width: 5.5rem; - height: 3.5rem; + max-width: 5.5rem; + max-height: 5.5rem; + aspect-ratio: var(--playlist-slide-thumb-aspect-ratio, 16 / 9); border-radius: 0.5rem; overflow: hidden; background: rgba(15, 23, 42, 0.08); diff --git a/src/web/public/js/playlists/playlist-schedule.js b/src/web/public/js/playlists/playlist-schedule.js index 8ba6cbe..450d4a9 100644 --- a/src/web/public/js/playlists/playlist-schedule.js +++ b/src/web/public/js/playlists/playlist-schedule.js @@ -308,6 +308,7 @@ var addSlideModal = document.getElementById('playlist-add-slide-modal'); var addSlideGrid = document.getElementById('playlist-slide-picker-grid'); var addSlideSearch = document.getElementById('playlist-slide-picker-search'); + var addSlideShowAssigned = document.getElementById('playlist-slide-picker-show-assigned'); var addSlideEmpty = document.getElementById('playlist-slide-picker-empty'); var addSlideConfirm = document.getElementById('playlist-confirm-add-slides'); var addSlideCount = document.getElementById('playlist-slide-picker-selected-count'); @@ -408,6 +409,7 @@ var media = document.createElement('div'); var title = document.createElement('div'); var check = document.createElement('span'); + var badge = document.createElement('span'); var searchText = String(slide && slide.title ? slide.title : '').toLowerCase(); button.type = 'button'; @@ -415,6 +417,7 @@ button.setAttribute('aria-pressed', 'false'); button.setAttribute('data-slide-id', String(slide.id || '')); button.setAttribute('data-search-text', searchText); + button.setAttribute('data-is-assigned', slide && slide.isAssigned ? 'true' : 'false'); if (slide && slide.isAssigned) { button.disabled = true; @@ -427,6 +430,11 @@ check.setAttribute('aria-hidden', 'true'); media.appendChild(check); + badge.className = 'playlist-slide-picker-badge badge text-bg-primary'; + badge.textContent = 'Already in playlist'; + badge.setAttribute('aria-hidden', 'true'); + media.appendChild(badge); + title.className = 'playlist-slide-picker-title'; title.textContent = String(slide && slide.title ? slide.title : 'Slide'); @@ -436,6 +444,16 @@ return button; } + function setShowAssignedState(showAssignedSlides) { + if (!addSlideShowAssigned) { + return; + } + + addSlideShowAssigned.setAttribute('aria-pressed', showAssignedSlides ? 'true' : 'false'); + addSlideShowAssigned.classList.toggle('btn-secondary', showAssignedSlides); + addSlideShowAssigned.classList.toggle('btn-outline-secondary', !showAssignedSlides); + } + function renderSlidePicker() { if (!addSlideGrid) { return; @@ -451,19 +469,31 @@ function syncSlidePickerState() { var query = String(addSlideSearch && addSlideSearch.value ? addSlideSearch.value : '').trim().toLowerCase(); + var showAssignedSlides = Boolean(addSlideShowAssigned && addSlideShowAssigned.getAttribute('aria-pressed') === 'true'); var visibleCount = 0; + var assignedCount = 0; slidePickerCards.forEach(function (card) { var searchText = String(card.getAttribute('data-search-text') || ''); - var matches = !query || searchText.indexOf(query) !== -1; + var isAssigned = card.getAttribute('data-is-assigned') === 'true'; + var matchesSearch = !query || searchText.indexOf(query) !== -1; + var matchesAssignment = showAssignedSlides || !isAssigned; + var matches = matchesSearch && matchesAssignment; setCardVisible(card, matches); + if (isAssigned) { + assignedCount += 1; + } if (matches) { visibleCount += 1; } }); if (addSlideEmpty) { - addSlideEmpty.textContent = query ? 'No slides match your search.' : 'No slides are currently available for this playlist.'; + if (!showAssignedSlides && assignedCount && visibleCount === 0) { + addSlideEmpty.textContent = query ? 'No available slides match your search.' : 'No available slides are currently left for this playlist.'; + } else { + addSlideEmpty.textContent = query ? 'No slides match your search.' : 'No slides are currently available for this playlist.'; + } addSlideEmpty.classList.toggle('is-hidden', visibleCount !== 0); } @@ -474,6 +504,8 @@ if (addSlideConfirm) { addSlideConfirm.disabled = slidePickerSelection.size === 0; } + + setShowAssignedState(showAssignedSlides); } function toggleCardSelection(card) { @@ -512,6 +544,8 @@ row_key: 'new-' + Date.now() + '-' + slide.id, slide_id: String(slide.id), thumbnail_path: String(slide.thumbnail_path || ''), + canvas_width: slide.canvas_width, + canvas_height: slide.canvas_height, canvas_signature: String(slide.canvasSignature || ''), title: slide.title || 'Slide', duration_seconds: 10, @@ -626,7 +660,10 @@ var isAssigned = Boolean(activeSlideIds[slideId]); var isCanvasMismatch = Boolean(allowedCanvasSignature && canvasSignature && canvasSignature !== allowedCanvasSignature); + slide.isAssigned = isAssigned; + if (card) { + card.setAttribute('data-is-assigned', isAssigned ? 'true' : 'false'); card.disabled = isAssigned || isCanvasMismatch; card.classList.toggle('is-assigned', isAssigned); card.classList.toggle('is-mismatch', isCanvasMismatch); @@ -736,6 +773,12 @@ var row = document.createElement('tr'); var rowKey = values.row_key || ('new-' + Date.now() + '-' + Math.random().toString(36).slice(2)); var playlistId = tbody.getAttribute('data-playlist-id') || ''; + var canvasWidth = Number(values.canvas_width); + var canvasHeight = Number(values.canvas_height); + var thumbnailStyle = ''; + if (Number.isFinite(canvasWidth) && Number.isFinite(canvasHeight) && canvasWidth > 0 && canvasHeight > 0) { + thumbnailStyle = ' style="--playlist-slide-thumb-aspect-ratio: ' + canvasWidth + ' / ' + canvasHeight + ';"'; + } var thumbnailMarkup = values.thumbnail_path ? '' : ''; @@ -752,7 +795,7 @@ '' + '' + '
' + - '' + + '' + '
' + values.title + '
' + '
' + '' + @@ -839,6 +882,14 @@ addSlideSearch.addEventListener('input', syncSlidePickerState); } + if (addSlideShowAssigned) { + addSlideShowAssigned.addEventListener('click', function () { + var nextState = addSlideShowAssigned.getAttribute('aria-pressed') !== 'true'; + setShowAssignedState(nextState); + syncSlidePickerState(); + }); + } + if (addSlideConfirm) { addSlideConfirm.addEventListener('click', function () { addSelectedSlides(); diff --git a/src/web/public/js/table-sort.js b/src/web/public/js/table-sort.js index 92da331..3c80de0 100644 --- a/src/web/public/js/table-sort.js +++ b/src/web/public/js/table-sort.js @@ -114,6 +114,10 @@ return false; } + if (headerCell.getAttribute && headerCell.getAttribute('data-sortable') === 'false') { + return false; + } + if (/actions?|buttons?/i.test(getTableHeaderText(headerCell))) { return false; } diff --git a/src/web/views/signage/playlists/edit.hbs b/src/web/views/signage/playlists/edit.hbs index b340e92..758833e 100644 --- a/src/web/views/signage/playlists/edit.hbs +++ b/src/web/views/signage/playlists/edit.hbs @@ -100,7 +100,7 @@
-