Compare commits

...

2 Commits

Author SHA1 Message Date
ske087 2539f82868 Fix Add Content card overlapping the playlist table
The two cards on the playlist manage page overlapped: the Add Content card
was painted on top of the playlist content card.

Cause: the 9-column playlist table needs ~830px, but its grid column was
only ~683px, so the table overflowed its card by ~150px. The Add Content
card comes later in the DOM, so it painted over the overflowing table.

This was introduced by the minmax(0, ...) I added in the previous commit
to stop a long filename widening the track. That fixed a page-wide
horizontal scrollbar but capped the track, which is what made the table
spill. Reverting alone would not have been correct either — it swaps the
overlap for the scrollbar — so this fixes the underlying width problem:

- Widen the page container. The template already declared max-width:1400px,
  but on its own wrapper — a child cannot widen its parent, so it did
  nothing and the layout was stuck at base.html's 1200px.
- Use 3fr/1fr instead of 2fr/1fr. At 2fr/1fr the table column came out
  ~50px too narrow and sat in a permanent horizontal scrollbar.
- Stack the cards below 1440px, where side-by-side cannot give the table
  the width it needs. Stacked, it fits outright.
- The stacked rule uses minmax(0, 1fr): a bare 1fr keeps the min-content
  floor, which let the table widen the track and push the whole page into
  horizontal overflow at <=900px.
- Trim cell padding 12px -> 8px to reclaim ~72px for the table.
- Keep .playlist-table-scroll (overflow-x:auto) as a last-resort clip for
  genuinely narrow screens, so the table can never paint over its
  neighbour again.

Verified by hit-testing the Add Content card at 500-1920px: zero elements
from the table are painted over it at any width. No page-wide horizontal
overflow at any width; the table fits without scrolling at >=1024px.
2026-10-14 21:54:45 +03:00
ske087 7784da495b 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.
2026-10-14 21:54:45 +03:00
@@ -39,9 +39,33 @@
.content-grid {
display: grid;
grid-template-columns: 2fr 1fr;
/* 3fr/1fr (not 2fr/1fr) so the 9-column playlist table gets enough room;
at 2fr/1fr it was ~50px too narrow and looped with a scrollbar.
minmax(0, …) removes the track's automatic min-content floor, without
which the table's intrinsic width would widen the track itself. */
grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
gap: 20px;
}
/* The table needs ~830px and side by side it cannot get that below ~1440px.
Stack the cards instead of forcing a horizontal scrollbar on the table. */
@media (max-width: 1439px) {
.content-grid {
/* minmax(0, 1fr) matters here too: a bare 1fr keeps the min-content
floor, which let the 830px table widen the track and push the
page into horizontal overflow. */
grid-template-columns: minmax(0, 1fr);
}
}
/* Last-resort containment: on a genuinely narrow screen the table cannot
fit, and an unclipped table is painted straight over the Add Content
card, because that card is later in the DOM and therefore paints on top.
Clip here so the table scrolls inside its own card instead. */
.playlist-table-scroll {
overflow-x: auto;
max-width: 100%;
}
.playlist-table {
width: 100%;
@@ -50,13 +74,13 @@
.playlist-table th {
background: #f8f9fa;
padding: 12px;
padding: 8px;
text-align: left;
border-bottom: 2px solid #dee2e6;
}
.playlist-table td {
padding: 12px;
padding: 8px;
border-bottom: 1px solid #dee2e6;
}
@@ -87,6 +111,56 @@
display: flex;
justify-content: space-between;
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 */
@@ -215,6 +289,15 @@
body.dark-mode .available-content {
color: #e2e8f0;
}
body.dark-mode .avail-thumb {
background: #2d3748;
border-color: #4a5568;
}
body.dark-mode .avail-meta {
color: #a0aec0;
}
/* Dark mode for duration spinner */
body.dark-mode .duration-display {
@@ -245,11 +328,18 @@
body.dark-mode .duration-spinner button.btn-decrease {
color: #f56565;
}
/* The page needs more than base.html's 1200px container to show the
playlist table and the Add Content card side by side. This template
already declared max-width:1400px on its own wrapper, but a child cannot
widen its parent, so it had no effect. Widen the base container instead. */
body > .container {
max-width: 1400px;
}
</style>
<div class="container" style="max-width: 1400px;">
<div class="playlist-header">
<h1>🎬 {{ playlist.name }}</h1>
<div class="playlist-header"> <h1>🎬 {{ playlist.name }}</h1>
{% if playlist.description %}
<p style="margin: 5px 0; opacity: 0.9;">{{ playlist.description }}</p>
{% endif %}
@@ -290,6 +380,7 @@
</div>
{% if playlist_content %}
<div class="playlist-table-scroll">
<table class="playlist-table" id="playlist-table">
<thead>
<tr>
@@ -397,6 +488,7 @@
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div style="text-align: center; padding: 40px; color: #999;">
<div style="font-size: 48px;">📭</div>
@@ -428,21 +520,32 @@
<div class="available-content">
{% for content in available_content %}
<div class="content-item">
<div>
<div>
{% if content.content_type == 'image' %}📷
{% elif content.content_type == 'video' %}🎥
{% elif content.content_type == 'pdf' %}📄
{% else %}📁{% endif %}
{{ content.original_display_name }}
<div class="avail-main">
<div class="avail-thumb">
{% if content.content_type == 'image' %}
{# Preview the real image so the user can see what
is being added. Falls back to the camera icon
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 style="font-size: 12px; color: #999;">
{{ content.file_size_mb }} MB
<div class="avail-info">
<div class="avail-name" title="{{ content.original_display_name }}">
{{ content.original_display_name }}
</div>
<div class="avail-meta">
{{ content.file_size_mb }} MB
</div>
</div>
</div>
<form method="POST"
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="duration" value="{{ content.duration }}">
<button type="submit" class="btn btn-success btn-sm">