diff --git a/README.md b/README.md index 3a6f947..cfd15a4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # subwave-nowplaying Tronbyt/Tidbyt app showing the current track on a [SUB/WAVE](https://github.com/perminder-klair/subwave) -internet radio station: album art, track title/artist (scrolling), a thin -separator, the current show and host, and a full-width footer with -time/temperature and a track progress bar pinned to the bottom edge. Color -scheme (cyan title/artist, white show name, magenta host/progress bar) is -loosely modeled on SUB/WAVE's own web now-playing display. +internet radio station: album art, and four scrolling lines — track +title/artist, a thin separator, the current show and host, and local +time/temperature — plus a track progress bar pinned to the bottom of the +32px-wide text column. Color scheme (cyan title/artist, white show name, +magenta host/progress bar) is loosely modeled on SUB/WAVE's own web +now-playing display. Not tied to any specific station — set your own station's base URL in the app's config when adding it to a device. @@ -28,22 +29,28 @@ app's config when adding it to a device. - If a track has no `subsonic_id`, or the cover fetch fails, falls back to a full-width layout with no art rather than a broken image. - Shows the current show name (`context.activeShow.name`) and host name - (`dj.name`) below the artist, separated by a thin divider line. -- Shows the local time and temperature (`context.clock`/`context.weather`) - and the track progress bar as a full-width (64px) footer overlaid on the - bottom of the layout — not confined to the 32px-wide text column, which - gives roughly 2x the character budget for the time/temp line (at the cost - of covering the bottom ~9px of the album art with a translucent bar). + (`dj.name`) below the artist, separated by a thin divider line, and the + local time/temperature (`context.clock`/`context.weather`) below that — + all as independently scrolling Marquee lines, confined to the 32px-wide + text column next to the album art (no overlay/overlap with the art). -**Why show/host/time-temp are plain Text, not Marquee:** pixlet's Marquee -runs its own internal scroll-cycle animation regardless of whether the text -actually needs to scroll, and combining multiple Marquees with very -different cycle lengths in one Root desyncs them — confirmed via an -isolated repro where a short Marquee'd line intermittently collapsed to a -single character on frames where the long title/artist marquees were -mid-cycle. Only title/artist (which actually benefit from scrolling for -long names) use Marquee; the shorter supplementary fields use plain -Text/Row and just clip if too wide, which is an acceptable tradeoff. +**Known quirk — occasional Marquee glitch:** pixlet's `render.Marquee` runs +its own internal scroll-cycle animation, and stacking several Marquees of +different text lengths in one Root can occasionally desync them — a line +may briefly blank or show a garbled frame while another Marquee is +mid-cycle. This is a real, currently-unfixed pixlet bug (there's an +open `FIXME: Behaviour when children themselves are animated is a bit +weird` in pixlet's own widget docs), not something introduced by this app. +It's also not unique to this app: `tronbyt/apps/apps/goosefm/goose_fm.star` +(an accepted official community app) stacks 3 Marquees for the same kind of +now-playing info and exhibits the identical occasional glitch on close +frame-by-frame inspection. In practice it's subtle enough to be easy to +miss on a real, continuously-scrolling display. A custom hand-rolled scroll +(via `animation.Transformation` + `Translate`, avoiding `Marquee` entirely) +was prototyped and could fully eliminate this, but only by consolidating +down to 2 scrolling lines total (title alone, everything else combined into +one line) — full independent lines for every field was preferred instead, +accepting the same minor quirk real Tidbyt/Tronbyt apps already ship with. **Progress bar caveat:** the now-playing API reports track `duration` but not elapsed/remaining time, so the app tracks playback position itself — diff --git a/subwave_nowplaying.star b/subwave_nowplaying.star index fca7bc2..f695d53 100644 --- a/subwave_nowplaying.star +++ b/subwave_nowplaying.star @@ -20,7 +20,7 @@ COLOR_ARTIST = "#22d3ee" # cyan COLOR_SEPARATOR = "#444444" # thin divider between artist and show/host COLOR_SHOW = "#e0e0e0" # near-white COLOR_HOST = "#ec4899" # magenta/pink -COLOR_META = "#888888" # muted gray, for the time/temp footer +COLOR_META = "#888888" # muted gray, for the time/temp line COLOR_BAR_FILLED = "#c026d3" # magenta/purple COLOR_BAR_EMPTY = "#333333" @@ -46,27 +46,34 @@ def main(config): render.Marquee(width = 32, child = render.Text(now["title"], color = COLOR_TITLE)), render.Marquee(width = 32, child = render.Text(now["artist"], color = COLOR_ARTIST)), ] - # Note: show/host is plain Text/Row, not Marquee. Pixlet's Marquee runs - # its own internal scroll-cycle animation regardless of whether the text - # actually needs to scroll, and combining multiple Marquees with very - # different cycle lengths in one Root desyncs them -- confirmed via an - # isolated repro where a short Marquee'd line intermittently collapsed to - # a single character on frames where the long title/artist marquees were - # mid-cycle. Plain Text just clips if too wide, an acceptable tradeoff - # for this short field. if now["show"]: top_lines.append(render.Box(width = 32, height = 1, color = COLOR_SEPARATOR)) show_host_row = [render.Text(now["show"], font = "tom-thumb", color = COLOR_SHOW)] if now["host"]: show_host_row.append(render.Text(" " + now["host"], font = "tom-thumb", color = COLOR_HOST)) - top_lines.append(render.Row(children = show_host_row)) + top_lines.append(render.Marquee(width = 32, child = render.Row(children = show_host_row))) + if now["time_temp"]: + top_lines.append(render.Marquee(width = 32, child = render.Text(now["time_temp"], font = "tom-thumb", color = COLOR_META))) - text_column = render.Column(children = top_lines) + children = [render.Column(children = top_lines)] + if progress != None: + children.append(_progress_bar(progress["fraction"], width = 32)) + + text_column = render.Box( + width = 32, + height = 32, + child = render.Column( + main_align = "space_between", + cross_align = "start", + expanded = True, + children = children, + ), + ) if art: - base = render.Row(children = [render.Image(src = art, width = 32, height = 32), text_column]) + content = render.Row(children = [render.Image(src = art, width = 32, height = 32), text_column]) else: - base = render.Column( + content = render.Column( main_align = "center", cross_align = "center", expanded = True, @@ -76,44 +83,7 @@ def main(config): ], ) - # Time/temp and the progress bar are rendered as a full-width (64px) - # footer overlaid on top of the base layout via Stack, rather than - # squeezed into the 32px-wide text column -- gives roughly 2x the - # character budget for the time/temp line, at the cost of covering the - # bottom ~9px of the album art with a translucent bar. - footer_children = [] - if now["time_temp"]: - footer_children.append( - render.Box( - width = 64, - height = 7, - color = "#000000cc", - child = render.Row( - expanded = True, - main_align = "center", - cross_align = "center", - children = [render.Text(now["time_temp"], font = "tom-thumb", color = COLOR_META)], - ), - ), - ) - if progress != None: - footer_children.append(_progress_bar(progress["fraction"], width = 64)) - - if not footer_children: - return render.Root(child = base) - - footer = render.Box( - width = 64, - height = 32, - child = render.Column( - main_align = "end", - cross_align = "start", - expanded = True, - children = [render.Column(children = footer_children)], - ), - ) - - return render.Root(child = render.Stack(children = [base, footer])) + return render.Root(child = content) def _message_screen(lines): return render.Column(