Show image thumbnails in the playlist Add Content card

The "Add Content" card listed available media as an icon plus a filename,
so the only way to tell two media items apart was to read near-identical
filenames. Images now render an actual thumbnail.

Previews are shown for every image type, not just JPEG: the library is a
mix of JPEG and PNG (the PDF/PPTX converter emits PNG), so a JPEG-only
rule would have left most items without a preview. Non-images keep the
existing emoji icon. A missing or unreadable file falls back to the icon
via onerror, and images are `loading="lazy"` since the list is scrollable.

This surfaced two layout bugs in the same card, both now fixed:

- .content-grid used bare `2fr 1fr`. A grid track has an automatic
  min-content floor, so the long unbreakable media filenames forced the
  track wider than the viewport — the page had a horizontal scrollbar
  (scrollWidth 1475 in a 1185px viewport). minmax(0, ...) fixes it.

- Long names were truncated with ellipsis, which hides the distinguishing
  part: media filenames differ at the END (..._slide_003.png). Names now
  wrap with overflow-wrap: anywhere instead.

The row is flex-wrap so that on a narrow card the Add button drops to its
own line rather than squeezing the name.

Verified: 5/5 items render an <img> whose URL returns HTTP 200 (including
a nested edited_media/ JPEG), no element overlaps at 1000-1500px, and dark
mode picks up the theme variables.
This commit is contained in:
2026-09-12 23:54:53 +03:00
parent 7177cdb9ab
commit 7784da495b
@@ -39,7 +39,11 @@
.content-grid { .content-grid {
display: grid; display: grid;
grid-template-columns: 2fr 1fr; /* minmax(0, …) is required: a bare `2fr 1fr` track has an automatic
min-content floor, so the long, unbreakable media filenames in the
Add Content card forced the track wider than the viewport and gave
the whole page a horizontal scrollbar. */
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
gap: 20px; gap: 20px;
} }
@@ -87,6 +91,56 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 12px;
/* On a narrow viewport the thumbnail + name + button no longer fit on
one line; wrapping drops the Add button below instead of squeezing
the name under it. */
flex-wrap: wrap;
}
/* Left group: thumbnail + name/size. min-width:0 lets long names ellipsise
instead of pushing the Add button out of the card. */
.avail-main {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.avail-thumb {
width: 52px;
height: 52px;
flex: 0 0 auto;
border-radius: 6px;
overflow: hidden;
background: #e9ecef;
border: 1px solid #dee2e6;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
line-height: 1;
}
.avail-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.avail-info {
min-width: 0;
}
.avail-name {
font-size: 14px;
/* Long media filenames carry the meaningful part at the END
(…_slide_003.png), so breaking mid-word is better than ellipsis,
which would hide the distinguishing characters. `anywhere` allows
breaks inside the unbroken run and keeps min-content width tiny. */
overflow-wrap: anywhere;
word-break: break-word;
}
.avail-meta {
font-size: 12px;
color: #999;
} }
/* Audio toggle styles */ /* Audio toggle styles */
@@ -216,6 +270,15 @@
color: #e2e8f0; color: #e2e8f0;
} }
body.dark-mode .avail-thumb {
background: #2d3748;
border-color: #4a5568;
}
body.dark-mode .avail-meta {
color: #a0aec0;
}
/* Dark mode for duration spinner */ /* Dark mode for duration spinner */
body.dark-mode .duration-display { body.dark-mode .duration-display {
background: #2d3748; background: #2d3748;
@@ -428,21 +491,32 @@
<div class="available-content"> <div class="available-content">
{% for content in available_content %} {% for content in available_content %}
<div class="content-item"> <div class="content-item">
<div> <div class="avail-main">
<div> <div class="avail-thumb">
{% if content.content_type == 'image' %}📷 {% if content.content_type == 'image' %}
{% elif content.content_type == 'video' %}🎥 {# Preview the real image so the user can see what
{% elif content.content_type == 'pdf' %}📄 is being added. Falls back to the camera icon
{% else %}📁{% endif %} if the file is missing or fails to load. #}
<img src="{{ url_for('static', filename='uploads/' + content.current_media_path) }}"
alt="{{ content.original_display_name }}"
loading="lazy"
onerror="this.parentElement.innerHTML='<span>📷</span>';">
{% elif content.content_type == 'video' %}<span>🎥</span>
{% elif content.content_type == 'pdf' %}<span>📄</span>
{% else %}<span>📁</span>{% endif %}
</div>
<div class="avail-info">
<div class="avail-name" title="{{ content.original_display_name }}">
{{ content.original_display_name }} {{ content.original_display_name }}
</div> </div>
<div style="font-size: 12px; color: #999;"> <div class="avail-meta">
{{ content.file_size_mb }} MB {{ content.file_size_mb }} MB
</div> </div>
</div> </div>
</div>
<form method="POST" <form method="POST"
action="{{ url_for('content.add_content_to_playlist', playlist_id=playlist.id) }}" action="{{ url_for('content.add_content_to_playlist', playlist_id=playlist.id) }}"
style="display: inline;"> style="display: inline; flex: 0 0 auto;">
<input type="hidden" name="content_id" value="{{ content.id }}"> <input type="hidden" name="content_id" value="{{ content.id }}">
<input type="hidden" name="duration" value="{{ content.duration }}"> <input type="hidden" name="duration" value="{{ content.duration }}">
<button type="submit" class="btn btn-success btn-sm"> <button type="submit" class="btn btn-success btn-sm">