diff --git a/apps/countupclock/countupclock.star b/apps/countupclock/countupclock.star index 7a80b5c72..6238c44ce 100644 --- a/apps/countupclock/countupclock.star +++ b/apps/countupclock/countupclock.star @@ -10,7 +10,7 @@ borrowed Fade In and Out technique and the math calculations from @CubsAaron cou # 20240802 - jvivona - added in code to handle widget mode and remove animations load("math.star", "math") -load("render.star", "render") +load("render.star", "canvas", "render") load("schema.star", "schema") load("time.star", "time") @@ -24,6 +24,21 @@ HOURS_COLOR = "#888888" WIDGET_MODE = False +# 2x (128x64) has the vertical room to wrap the title across more lines and show +# hours and minutes as their own static lines instead of the fading marquee. +IS2X = canvas.is2x() +TITLE_FONT_2X = "6x13" + +# Keep the title to 2 lines. We greedily word-wrap it into at most two 21-char +# lines (6x13 is 6px wide, so 128px holds 21 chars) and render each as its own +# Text line - shrink-wrapped and vertically centered in its half with tight +# spacing (a fixed WrappedText height would top-align a single line instead). +# Longer titles get an ellipsis. +LINE_CHARS_2X = 21 +DAYS_FONT_2X = "terminus-16" +HOURS_FONT_2X = "tb-8" +HOURS_COLOR_2X = "#AAAAAA" + coloropt = [ schema.Option( display = "Red", @@ -82,10 +97,10 @@ def main(config): ) def get_render_children(config, widgetMode): - render_children = [] displayhours = config.bool("display_hours", True) displayminutes = config.bool("display_minutes", True) if displayhours else False titlebelow = config.bool("title_below", False) + is2x = IS2X and not widgetMode current_time = time.now().in_location(time.tz()) origin_time = time.parse_time(config.str("event_time", current_time.format("2006-01-02T15:04:05Z07:00"))) @@ -95,7 +110,12 @@ def get_render_children(config, widgetMode): days = math.floor(datediff.hours // 24) daystring = "{} {}".format(str(days), "Day" if days == 1 else "Days") - render_children.append(render.Text(content = daystring, font = DAYS_FONT if not (widgetMode and displayhours) else HOURS_FONT)) + if is2x: + return get_2x_children(config, datediff, days, daystring, displayhours, displayminutes, titlebelow) + + render_children = [] + days_font = HOURS_FONT if (widgetMode and displayhours) else DAYS_FONT + render_children.append(render.Text(content = daystring, font = days_font)) if displayhours: render_children.append(get_hours_minutes(datediff, days, displayminutes, widgetMode)) @@ -106,6 +126,67 @@ def get_render_children(config, widgetMode): return render_children +def title_lines_2x(text): + # Greedy word-wrap into at most two 21-char lines; ellipsis if it overflows. + lines = ["", ""] + li = 0 + truncated = False + for w in text.split(): + cand = w if lines[li] == "" else lines[li] + " " + w + if len(cand) <= LINE_CHARS_2X: + lines[li] = cand + elif li == 0: + li = 1 + lines[1] = w + else: + truncated = True + break + + # guard against a single word wider than a line + lines = [ln[:LINE_CHARS_2X] for ln in lines] + if truncated: + tail = lines[1] + if len(tail) >= LINE_CHARS_2X: + tail = tail[:LINE_CHARS_2X - 1] + lines[1] = tail + "…" + + return [ln for ln in lines if ln != ""] + +def get_2x_children(config, datediff, days, daystring, displayhours, displayminutes, titlebelow): + # Split the 128x64 canvas into two equal 32px halves - the title in one and + # the day/hour/minute counts in the other - each vertically centered in its + # half (render.Box centers its child). The larger title is clamped to 2 lines. + titlecolor = config.str("event_color", coloropt[3].value) + title_box = render.Box( + width = 128, + height = 32, + child = render.Column( + main_align = "center", + cross_align = "center", + children = [ + render.Text(content = line, font = TITLE_FONT_2X, color = titlecolor) + for line in title_lines_2x(config.str("event", "")) + ], + ), + ) + + count_lines = [render.Text(content = daystring, font = DAYS_FONT_2X)] + if displayhours: + count_lines.extend(get_hours_minutes_2x(datediff, days, displayminutes)) + count_box = render.Box( + width = 128, + height = 32, + child = render.Column( + main_align = "center", + cross_align = "center", + children = count_lines, + ), + ) + + if titlebelow: + return [count_box, title_box] + return [title_box, count_box] + def get_title(eventtitle, titlecolor, displayhours, widgetMode): if displayhours and not widgetMode: # since we are displaying hours - title needs to be marquee - text less than width will center on screen @@ -152,6 +233,25 @@ def get_hours_minutes(datediff, days, displayminutes, widgetMode): expanded = True, ) +def get_hours_minutes_2x(datediff, days, displayminutes): + # 2x static lines: no fade animation - just stack hours (and minutes) below days + hours = math.floor(datediff.hours - days * 24) + lines = [ + render.Text( + content = "{} {}".format(str(hours), "Hour" if hours == 1 else "Hours"), + font = HOURS_FONT_2X, + color = HOURS_COLOR_2X, + ), + ] + if displayminutes: + minutes = math.floor(datediff.minutes - (days * 24 * 60 + hours * 60)) + lines.append(render.Text( + content = "{} {}".format(str(minutes), "Minute" if minutes == 1 else "Minutes"), + font = HOURS_FONT_2X, + color = HOURS_COLOR_2X, + )) + return lines + def createfadelist(text, cycles): alpha_values = ["00", "33", "66", "99", "CC", "FF"] cycle_list = [] diff --git a/apps/countupclock/countupclock.webp b/apps/countupclock/countupclock.webp index 38af8c8e8..c6f36fe82 100644 Binary files a/apps/countupclock/countupclock.webp and b/apps/countupclock/countupclock.webp differ diff --git a/apps/countupclock/countupclock@2x.webp b/apps/countupclock/countupclock@2x.webp new file mode 100644 index 000000000..cd2fc58d9 Binary files /dev/null and b/apps/countupclock/countupclock@2x.webp differ diff --git a/apps/countupclock/manifest.yaml b/apps/countupclock/manifest.yaml index 1f1291f8e..b024c1d80 100644 --- a/apps/countupclock/manifest.yaml +++ b/apps/countupclock/manifest.yaml @@ -7,10 +7,11 @@ author: jvivona fileName: countupclock.star packageName: countupclock recommendedInterval: 0 +supports2x: true category: clocks tags: - time - utility - tracking published: '2022-11-17T20:03:31Z' -updated: '2025-12-02T19:30:22Z' +updated: '2026-07-06T16:50:05Z' diff --git a/apps/passover/images/cup.png b/apps/passover/images/cup.png new file mode 100644 index 000000000..56b785f7b Binary files /dev/null and b/apps/passover/images/cup.png differ diff --git a/apps/passover/images/star.png b/apps/passover/images/star.png new file mode 100644 index 000000000..43c312eb4 Binary files /dev/null and b/apps/passover/images/star.png differ diff --git a/apps/passover/manifest.yaml b/apps/passover/manifest.yaml index d6a90e53d..c2caf2141 100644 --- a/apps/passover/manifest.yaml +++ b/apps/passover/manifest.yaml @@ -5,9 +5,10 @@ summary: Passover Countdown / In Progress desc: Track days until Passover show current when it is in progress. author: jvivona recommendedInterval: 5 +supports2x: true category: lifestyle tags: - time - lifestyle published: '2026-03-20T00:15:10Z' -updated: '2026-03-20T00:15:10Z' +updated: '2026-07-06T16:50:15Z' diff --git a/apps/passover/passover.star b/apps/passover/passover.star index c1d837dc4..d02b90996 100644 --- a/apps/passover/passover.star +++ b/apps/passover/passover.star @@ -5,9 +5,18 @@ Description: Shows countdown to Passover or which day of the 8-day celebration i Author: jvivona """ -load("render.star", "render") +load("images/cup.png", CUP = "file") +load("images/star.png", STAR = "file") +load("render.star", "canvas", "render") load("time.star", "time") +# 2x (128x64) adds a Star of David + Kiddush cup and an expanded date line; +# 1x (64x32) is unchanged. +IS2X = canvas.is2x() + +STAR_IMG = STAR.readall() +CUP_IMG = CUP.readall() + PASSOVER_DATES = [ {"year": 2026, "start": "2026-04-01", "end": "2026-04-08"}, {"year": 2027, "start": "2027-04-22", "end": "2027-04-29"}, @@ -24,6 +33,7 @@ PASSOVER_DATES = [ # Hebrew text for Passover PASSOVER_HEBREW = "פסח" +CHAG_SAMEACH = "חג שמח" def main(): now = time.now().in_location(time.tz()) @@ -53,6 +63,40 @@ def main(): else: return render_default() +# --- 2x sprite helpers ------------------------------------------------------- + +def top_row_2x(with_cup): + """Star of David + Hebrew word, optionally flanked by the Kiddush cup.""" + children = [ + render.Image(src = STAR_IMG), + render.Box(width = 5, height = 1), + render.Text(content = PASSOVER_HEBREW, font = "6x13", color = "#FFD700"), + ] + if with_cup: + children.append(render.Box(width = 5, height = 1)) + children.append(render.Image(src = CUP_IMG)) + return render.Row( + main_align = "center", + cross_align = "center", + children = children, + ) + +def frame_2x(children): + return render.Root( + child = render.Box( + width = 128, + height = 64, + child = render.Column( + expanded = True, + main_align = "space_evenly", + cross_align = "center", + children = children, + ), + ), + ) + +# --- During Passover --------------------------------------------------------- + def render_during_passover(passover, now, timezone): """Render display during Passover showing which day it is""" start_time = time.parse_time(passover["start"] + "T00:00:00Z").in_location(timezone) @@ -75,6 +119,22 @@ def render_during_passover(passover, now, timezone): day_name = day_names[day_of_passover - 1] + if IS2X: + return frame_2x([ + top_row_2x(True), + render.Text(content = CHAG_SAMEACH, font = "6x13", color = "#87CEEB"), + render.Text(content = day_name, font = "5x8", color = "#FFFFFF"), + render.Row( + main_align = "center", + cross_align = "center", + children = [ + render.Text(content = "Day ", font = "tb-8", color = "#98D8C8"), + render.Text(content = str(day_of_passover), font = "tb-8", color = "#FFD700"), + render.Text(content = " of 8", font = "tb-8", color = "#98D8C8"), + ], + ), + ]) + return render.Root( child = render.Box( child = render.Column( @@ -139,6 +199,8 @@ def render_during_passover(passover, now, timezone): ), ) +# --- Countdown --------------------------------------------------------------- + def render_countdown(passover, now, timezone): """Render countdown to next Passover""" start_time = time.parse_time(passover["start"] + "T00:00:00Z").in_location(timezone) @@ -147,6 +209,29 @@ def render_countdown(passover, now, timezone): time_until = start_time - now days_until = int(time_until.hours / 24) + if IS2X: + # Parse at noon UTC so the displayed calendar date never shifts a day + # under a negative UTC offset (e.g. America/New_York). + start_disp = time.parse_time(passover["start"] + "T12:00:00Z").in_location(timezone) + return frame_2x([ + top_row_2x(True), + render.Row( + cross_align = "center", + children = [ + render.Text(content = str(days_until), font = "6x13", color = "#FFD700"), + render.Text(content = " days", font = "6x13", color = "#FFFFFF"), + ], + ), + render.Column( + cross_align = "center", + children = [ + render.Text(content = start_disp.format("Monday"), font = "5x8", color = "#FFFFFF"), + render.Text(content = start_disp.format("January 2"), font = "tom-thumb", color = "#FFFFFF"), + render.Text(content = str(passover["year"]), font = "tom-thumb", color = "#98D8C8"), + ], + ), + ]) + return render.Root( child = render.Box( child = render.Column( @@ -206,8 +291,16 @@ def render_countdown(passover, now, timezone): ), ) +# --- Default ----------------------------------------------------------------- + def render_default(): """Default render if no Passover data available""" + if IS2X: + return frame_2x([ + top_row_2x(True), + render.Text(content = "Passover", font = "6x13", color = "#FFFFFF"), + ]) + return render.Root( child = render.Box( child = render.Column( diff --git a/apps/passover/passover.webp b/apps/passover/passover.webp new file mode 100644 index 000000000..364f53f70 Binary files /dev/null and b/apps/passover/passover.webp differ diff --git a/apps/passover/passover@2x.webp b/apps/passover/passover@2x.webp new file mode 100644 index 000000000..94cfba62c Binary files /dev/null and b/apps/passover/passover@2x.webp differ