From 5baa45904c629fcc0815908382e67cb3e212976a Mon Sep 17 00:00:00 2001 From: Honey Date: Tue, 21 Jul 2026 23:25:44 +0200 Subject: [PATCH] Fix off-center Navidrome layout Two separate imbalances: - Cover art was top-aligned when width-constrained (landscape, cover_size capped by width*0.5), leaving dead space below it instead of centering in the available height. - The text column always started drawing at the very top and let the source line's bottom-pin create a big empty gap for tracks with short/sparse metadata -- looked top-heavy and unbalanced. Text column is now built as (height, draw_fn) pairs first so the total height is known before committing to a starting y, then centered in the space above the reserved source-line area. --- screens/navidrome.py | 96 +++++++++++++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 29 deletions(-) diff --git a/screens/navidrome.py b/screens/navidrome.py index beb7095..1a257f5 100644 --- a/screens/navidrome.py +++ b/screens/navidrome.py @@ -199,8 +199,10 @@ class NavidromeScreen(Screen): # Cap at a fraction of width too, not just height -- a naive full-height # square cover can eat almost the whole canvas on a wide/short panel and # leave the text column too narrow to wrap sensibly. - cover_size = min(height - 2 * pad, int(width * 0.5)) - cover_x, cover_y = pad, pad + available_h = height - 2 * pad + cover_size = min(available_h, int(width * 0.5)) + cover_x = pad + cover_y = pad + (available_h - cover_size) // 2 # center vertically when width-constrained self._paste_cover(image, cover, cover_x, cover_y, cover_size) @@ -244,52 +246,89 @@ class NavidromeScreen(Screen): tw = text_width(draw, text, font) return x + (w - tw) / 2 if center else x - cursor = y + # Reserve space for the bottom-pinned source line up front, so the + # block above can be vertically centered in what's actually left + # rather than always starting at the very top -- hugging the top + # left a large, unbalanced gap above the source line whenever the + # track had short/sparse metadata. + show_source = settings.get("show_source", True) + source_reserved_h = source_font.size * 1.6 if show_source else 0 + content_h = h - source_reserved_h + + # Build the content block as (height, draw_fn) pairs first so its + # total height is known before committing to a starting y. + items: list[tuple[float, object]] = [] status = "ZULETZT GESPIELT" if idle else "● NOW PLAYING" - draw.text((centered_x(status, status_font), cursor), status, font=status_font, fill=(100, 100, 100)) - cursor += status_font.size * 1.8 + + def draw_status(yy): + draw.text((centered_x(status, status_font), yy), status, font=status_font, fill=(100, 100, 100)) + + items.append((status_font.size * 1.8, draw_status)) title = track["title"] + (" ★" if track["starred"] else "") for line in wrap_to_lines(draw, title, title_font, w, max_lines=2): - draw.text((centered_x(line, title_font), cursor), line, font=title_font, fill="black") - cursor += title_font.size * 1.15 + def draw_title_line(yy, line=line): + draw.text((centered_x(line, title_font), yy), line, font=title_font, fill="black") + items.append((title_font.size * 1.15, draw_title_line)) + if items: + items[-1] = (items[-1][0] + title_font.size * 0.2, items[-1][1]) - cursor += title_font.size * 0.2 artist_line = truncate_to_width(draw, track["artist"], artist_font, w) - draw.text((centered_x(artist_line, artist_font), cursor), artist_line, font=artist_font, fill=(30, 30, 30)) - cursor += artist_font.size * 1.4 + + def draw_artist(yy): + draw.text((centered_x(artist_line, artist_font), yy), artist_line, font=artist_font, fill=(30, 30, 30)) + + items.append((artist_font.size * 1.4, draw_artist)) if track["album"]: album_line = track["album"] if track["year"]: album_line += f" · {track['year']}" album_line = truncate_to_width(draw, album_line, album_font, w) - draw.text((centered_x(album_line, album_font), cursor), album_line, font=album_font, fill=(110, 110, 110)) - cursor += album_font.size * 1.4 + + def draw_album(yy, album_line=album_line): + draw.text((centered_x(album_line, album_font), yy), album_line, font=album_font, fill=(110, 110, 110)) + + items.append((album_font.size * 1.4, draw_album)) if settings.get("show_genre", True) and track["genre"]: genre_line = truncate_to_width(draw, track["genre"].upper(), album_font, w) - draw.text((centered_x(genre_line, album_font), cursor), genre_line, font=album_font, fill=(150, 150, 150)) - cursor += album_font.size * 1.4 + + def draw_genre(yy, genre_line=genre_line): + draw.text((centered_x(genre_line, album_font), yy), genre_line, font=album_font, fill=(150, 150, 150)) + + items.append((album_font.size * 1.4, draw_genre)) if settings.get("show_progress", False) and not idle and track["duration"]: - bar_y = cursor + status_font.size * 0.3 bar_h = max(3, int(scale * 0.012)) - draw.rectangle((x, bar_y, x + w, bar_y + bar_h), outline=(60, 60, 60)) - fill_w = int(w * min(1.0, track["progress_pct"])) - if fill_w > 0: - draw.rectangle((x, bar_y, x + fill_w, bar_y + bar_h), fill=(60, 60, 60)) - cursor = bar_y + bar_h + status_font.size * 0.4 - times = f"{track['elapsed_text']} / {track['duration_text']}" - draw.text((centered_x(times, source_font), cursor), times, font=source_font, fill=(110, 110, 110)) - cursor += source_font.size * 1.6 + bar_gap = status_font.size * 0.3 + + def draw_progress(yy, bar_h=bar_h, bar_gap=bar_gap): + bar_y = yy + bar_gap + draw.rectangle((x, bar_y, x + w, bar_y + bar_h), outline=(60, 60, 60)) + fill_w = int(w * min(1.0, track["progress_pct"])) + if fill_w > 0: + draw.rectangle((x, bar_y, x + fill_w, bar_y + bar_h), fill=(60, 60, 60)) + times = f"{track['elapsed_text']} / {track['duration_text']}" + draw.text((centered_x(times, source_font), bar_y + bar_h + status_font.size * 0.4), times, font=source_font, fill=(110, 110, 110)) + + items.append((bar_gap + bar_h + status_font.size * 0.4 + source_font.size * 1.6, draw_progress)) elif track["duration"]: duration_badge = track["duration_text"] - draw.text((centered_x(duration_badge, source_font), cursor), duration_badge, font=source_font, fill=(110, 110, 110)) - cursor += source_font.size * 1.6 - if settings.get("show_source", True): + def draw_duration(yy, duration_badge=duration_badge): + draw.text((centered_x(duration_badge, source_font), yy), duration_badge, font=source_font, fill=(110, 110, 110)) + + items.append((source_font.size * 1.6, draw_duration)) + + total_h = sum(item_h for item_h, _ in items) + cursor = y + max(0.0, (content_h - total_h) / 2) + for item_h, draw_fn in items: + draw_fn(cursor) + cursor += item_h + + if show_source: if idle: source_line = f"vor {track['minutes_ago']} min" else: @@ -298,9 +337,8 @@ class NavidromeScreen(Screen): if source_line: source_line = truncate_to_width(draw, source_line, source_font, w) # Prefer pinning to the bottom of the available area, but never - # overlap whatever the cursor already drew above it -- long - # titles/metadata can push past where a fixed bottom position - # would otherwise land. + # overlap whatever the block above drew -- long titles/metadata + # can still push past where a fixed bottom position would land. bottom_y = max(cursor + status_font.size * 0.4, y + h - source_font.size * 1.3) draw.text((centered_x(source_line, source_font), bottom_y), source_line, font=source_font, fill=(140, 140, 140))