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 @@
'' +
'