Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ as the release notes.

## [Unreleased]

### Added

- A video in a post shows its poster frame, decoded at the size it is drawn (400 px at most), with a line saying it cannot play on the 3DS. The poster and its shape come from Wolfram v0.37.0's `wf_post_embed`. ([#69](https://github.com/ewanc26/indigo/pull/69))

## [0.11.0] - 2026-10-07

What has and has not been run, plainly: the host tests pass (4029 checks) and the 3DS build links against Wolfram v0.36.2. The new tabs have not been run on a 3DS or in Azahar.
Expand Down
2 changes: 1 addition & 1 deletion docs/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ An unknown pairing code (a restarted node forgets its pairings) used to be polle
| Attach an image when composing | yes | implemented: SELECT or the "Add an image" button in compose opens a picker over `sdmc:/3ds/indigo/images` (JPEG and PNG under 950 KB), then asks for alt text; works on posts, replies and quotes. The type and size filter, the folder scan and the upload are Wolfram's (`wolfram/attach.h`) | host only: the compose state, the picker and the layouts (snapshots `compose-image`, `attach-picker`). The upload and a real post have not been run on an emulator or a 3DS (#19 stays open for that). Whether the picker should also look in `sdmc:/DCIM`, where the camera saves, is undecided and needs a console |
| On-card cache of timeline and profile data | not listed | issue: #13 | not started |
| Settings screen | n/a | implemented: theme, text scale, alt text, diagnostics, startup feed; reduce motion and high contrast are stored and reserved | host |
| Video, GIFs and animated media | not planned | not planned: Wolfram's decoder (`wf_image_decode_rgba`) is for still images, and nothing in the 1.5MB image budget accounts for frames | no decoder evaluation has been done, so this is a decision and not a hardware limit |
| Video, GIFs and animated media | not planned | partial: a video shows its poster frame, decoded at the draw size (400 px cap), with a line saying it cannot play on the 3DS (#69). GIFs are not drawn. | Wolfram's decoder (`wf_image_decode_rgba`) is for still images, and nothing in the 1.5MB image budget accounts for frames. No decoder evaluation has been done, so playback is a decision and not a hardware limit. |
| Auto-update | in progress, to the same wolfram#106 contract | implemented: More > "Check for updates" asks GitHub for the latest release, shows the version and asks before downloading. The download is checked (size and SHA-256), written, re-read from the card, and swapped in with the old build kept until the new one starts. Manifest signed (Ed25519, `update.json.sig`) and refused if unsigned | host only (the screen's states, the pinned URLs, and the swap and its recovery with power lost at every write; docs/UPDATE.md). The network, the card and the swap have not been run on an emulator or hardware (#25) |
| Push notifications | not planned | not planned: no push service a homebrew application can register with; notifications are fetched when the screen is opened | |

Expand Down
3 changes: 2 additions & 1 deletion src/app/timeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ typedef enum {

/* What a post carries, for drawing rather than for the summary line. A kind of
* NONE with a non-empty embed_note is a quote or an attachment Indigo draws as
* text; IMAGE and LINK are drawn as themselves. */
* text; IMAGE, LINK and VIDEO are drawn as themselves. */
typedef enum {
INDIGO_EMBED_NONE = 0,
INDIGO_EMBED_IMAGE,
INDIGO_EMBED_LINK,
INDIGO_EMBED_VIDEO, /* the poster frame; Indigo cannot play the video */
} indigo_embed_kind;

typedef struct {
Expand Down
41 changes: 31 additions & 10 deletions src/atproto/session_view.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ indigo_session_count_of(int v)
return v > 0 ? (unsigned) v : 0;
}

/* The declared aspect ratio, halved until each side fits a byte. */
static void
set_embed_shape(indigo_post *out, int width, int height)
{
unsigned w;
unsigned h;

if (width <= 0 || height <= 0) {
return;
}
w = (unsigned) width;
h = (unsigned) height;
while (w > 255 || h > 255) {
w /= 2;
h /= 2;
}
out->embed_w = (unsigned char) (w ? w : 1);
out->embed_h = (unsigned char) (h ? h : 1);
}

/* Carry the embed onto the post, for the screens that draw it. The
* display helper owns the safe text/facet view; the typed embed reader supplies
* image metadata that the display summary deliberately keeps bounded to a count. */
Expand Down Expand Up @@ -77,16 +97,7 @@ fill_embed(const wf_post_display *d, const cJSON *raw_embed, indigo_post *out)
indigo_media_cdn_url(out->embed_thumb, sizeof out->embed_thumb, img->thumb, INDIGO_CDN_THUMBNAIL);
indigo_copy_utf8(out->embed_alt, sizeof out->embed_alt,
img->alt ? img->alt : "");
if (img->width > 0 && img->height > 0) {
unsigned w = (unsigned) img->width;
unsigned h = (unsigned) img->height;
while (w > 255 || h > 255) {
w /= 2;
h /= 2;
}
out->embed_w = (unsigned char) (w ? w : 1);
out->embed_h = (unsigned char) (h ? h : 1);
}
set_embed_shape(out, img->width, img->height);
break;
}
}
Expand All @@ -102,6 +113,16 @@ fill_embed(const wf_post_display *d, const cJSON *raw_embed, indigo_post *out)
INDIGO_CDN_THUMBNAIL);
}

/* A video's poster frame is a picture of the right shape; the video itself
* is not played, and the layout says so. */
if (out->embed_kind == INDIGO_EMBED_NONE && embed.video_thumb &&
embed.video_thumb[0]) {
out->embed_kind = INDIGO_EMBED_VIDEO;
indigo_media_cdn_url(out->embed_thumb, sizeof out->embed_thumb, embed.video_thumb,
INDIGO_CDN_THUMBNAIL);
set_embed_shape(out, embed.video_width, embed.video_height);
}

wf_post_embed_free(&embed);
}

Expand Down
15 changes: 11 additions & 4 deletions src/ui/layout_post.c
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ draw_post_text(indigo_canvas *c, const indigo_post *p, unsigned max_lines,
}

/* True when the post has an embed the screen draws as itself rather than as a
* line of text. A quote, a video and an attachment Indigo cannot draw all keep
* the one-line note instead. */
* line of text. A quote and an attachment Indigo cannot draw keep the one-line
* note instead; a video is drawn as its poster frame. */
static bool
post_draws_embed(const indigo_post *p)
{
if (p->embed_kind == INDIGO_EMBED_IMAGE) {
if (p->embed_kind == INDIGO_EMBED_IMAGE || p->embed_kind == INDIGO_EMBED_VIDEO) {
return p->embed_thumb[0] != '\0';
}
if (p->embed_kind == INDIGO_EMBED_LINK) {
Expand Down Expand Up @@ -222,10 +222,17 @@ indigo_layout_draw_post_body(indigo_canvas *c, const indigo_post *p, bool show_a

draw_post_text(c, p, embed ? POST_TEXT_LINES_EMBED : POST_TEXT_LINES, text_scale);
if (embed) {
if (p->embed_kind != INDIGO_EMBED_IMAGE) {
if (p->embed_kind == INDIGO_EMBED_LINK) {
draw_post_link(c, p);
return;
}
if (p->embed_kind == INDIGO_EMBED_VIDEO) {
/* The label takes its line off the band before the poster is
* fitted into what is left, so the two never overlap. */
band_h = EMBED_H - draw_post_alt(c, "Video: it can't play on the 3DS") - 4.0f;
draw_post_image(c, p, band_h < EMBED_H_MIN ? EMBED_H_MIN : band_h);
return;
}
if (show_alt && p->embed_alt[0]) {
/* The alt text sits at the bottom of the band, so the picture is
* fitted into what is above it. */
Expand Down
44 changes: 44 additions & 0 deletions tests/tests.c
Original file line number Diff line number Diff line change
Expand Up @@ -2290,6 +2290,49 @@ test_layout_draws_post_images(void)
}
}

/* A video is drawn as its poster frame with a line saying it cannot play, and
* the line sits under the poster rather than on it. */
static void
test_layout_draws_video_poster(void)
{
static const char *const poster = "https://video.example/hls/did:plc:one/c/thumbnail.jpg";
indigo_app app;
indigo_input in = {0};
indigo_canvas top;
indigo_canvas bottom;
indigo_post *p;
const indigo_cmd *img;

indigo_app_init(&app);
app.screen = INDIGO_SCREEN_HOME;
app.timeline.count = 1;
p = &app.timeline.posts[0];
indigo_copy_utf8(p->text, sizeof p->text, "Watch this.");
indigo_copy_utf8(p->embed_note, sizeof p->embed_note, "[video]");
indigo_copy_utf8(p->embed_thumb, sizeof p->embed_thumb, poster);
p->embed_kind = INDIGO_EMBED_VIDEO;
p->embed_w = 16;
p->embed_h = 9;

indigo_layout_build(&app, &in, &top, &bottom);
CHECK(!top.overflow);
img = find_image(&top, poster);
CHECK(img != NULL);
CHECK(text_has(&top, "can't play"));
CHECK(!text_has(&top, "[video]"));
for (unsigned i = 0; i < top.count && img; i++) {
if (top.cmds[i].kind == INDIGO_CMD_TEXT &&
strstr(top.text + top.cmds[i].text_offset, "can't play")) {
CHECK(top.cmds[i].y >= img->y + img->h);
}
}

/* No poster URL, nothing to draw: the one-line note stands in for it. */
p->embed_thumb[0] = '\0';
indigo_layout_build(&app, &in, &top, &bottom);
CHECK(text_has(&top, "[video]"));
}

static void
test_layout_draws_link_cards(void)
{
Expand Down Expand Up @@ -5394,6 +5437,7 @@ main(void)
test_shapes_stay_on_screen();
test_settings_screen_shows_the_build();
test_layout_draws_post_images();
test_layout_draws_video_poster();
test_layout_draws_link_cards();
test_layout_draws_alt_text();
test_layout_text_scale();
Expand Down
16 changes: 16 additions & 0 deletions tools/snapshot.c
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,21 @@ fill_timeline(indigo_timeline *t, int kind, unsigned select)
indigo_timeline_fail_fetch(t, "Could not reach the network.");
return;
}
if (kind == 4) {
/* One video post: its poster, shaped 16:9 the way the server declares
* it, and the line saying it cannot play. The host has no pixels, so
* the poster is the placeholder, which is the layout that matters. */
indigo_post *p;

add_post(t, "Video", "video.example", "A clip from the river walk.", "", 3, false, false);
p = &t->posts[0];
p->embed_kind = INDIGO_EMBED_VIDEO;
snprintf(p->embed_thumb, sizeof p->embed_thumb,
"https://video.bsky.app/watch/did:plc:fake/bafyvideo/thumbnail.jpg");
p->embed_w = 16;
p->embed_h = 9;
return;
}
if (kind == 13) {
/* Posts and a failure at the same time, which is the state the header
* bar's status text and the viewer button have to share. */
Expand Down Expand Up @@ -795,6 +810,7 @@ main(int argc, char **argv)
{"timeline-scrolled", INDIGO_SCREEN_HOME, false, 0, 0, 1, 4, false},
{"timeline-loading", INDIGO_SCREEN_HOME, false, 0, 0, 2, 0, false},
{"timeline-error", INDIGO_SCREEN_HOME, false, 0, 0, 3, 0, false},
{"timeline-video", INDIGO_SCREEN_HOME, false, 0, 0, 4, 0, false},
{"thread", INDIGO_SCREEN_THREAD, false, 0, 0, 1, 1, false},
{"profile", INDIGO_SCREEN_PROFILE, false, 0, 0, 0, 0, false},
{"profile-follow", INDIGO_SCREEN_PROFILE, false, 0, 0, 1, 0, false},
Expand Down
2 changes: 1 addition & 1 deletion wolfram.ref
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.36.2
v0.37.0
Loading