From b14ae0a5954615e6beefa7667b8600e2a8a85074 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Thu, 8 Oct 2026 00:26:04 +0100 Subject: [PATCH 1/3] feat(app): list the camera folder in the image picker The picker listed only sdmc:/3ds/indigo/images. The 3DS camera saves photos as sdmc:/DCIM//, so the picker now also lists the images one folder down in sdmc:/DCIM, after the app folder's own. Each camera row is marked, and choosing one attaches its full path. The app folder scan is unchanged. The top screen names both folders. The camera scan uses Wolfram's wf_attach_scan_images_tree, which needs Wolfram v0.39.0 (not released yet). The pin stays at v0.38.0 on main. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 4 ++ docs/PARITY.md | 2 +- src/app/app.c | 1 + src/app/app.h | 5 +- src/app/app_screens.c | 5 +- src/app/social.c | 32 +++++++++---- src/app/social.h | 16 +++++-- src/ui/layout_top.c | 3 +- tests/tests.c | 105 ++++++++++++++++++++++++++++++++++++++++++ tools/snapshot.c | 14 +++++- 10 files changed, 167 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4774dad..ac359fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ as the release notes. ## [Unreleased] +### Added + +- The image picker also lists the pictures the 3DS camera saves in `sdmc:/DCIM`, one folder down (`DCIM//`), after the pictures in `sdmc:/3ds/indigo/images`. Choosing one attaches it from its full path. The picker's top screen names both folders. This needs Wolfram v0.39.0's `wf_attach_scan_images_tree`, which is not released yet, so this PR is blocked on that release. + ### Changed - Signing in with an app password needs only the handle: Indigo finds the account's PDS from it, so the Service field is not needed. An empty Service starts from https://bsky.social. The Service field is still used for the browser (OAuth node) sign-in. This needs Wolfram v0.38.0's `wf_agent_login_discovered`, which is not released yet, so this PR is blocked on that release. ([#72](https://github.com/ewanc26/indigo/pull/72)) diff --git a/docs/PARITY.md b/docs/PARITY.md index 6ebf096..0569729 100644 --- a/docs/PARITY.md +++ b/docs/PARITY.md @@ -51,7 +51,7 @@ An unknown pairing code (a restarted node forgets its pairings) used to be polle | Lists and members | read-only | implemented, read-only | host | | Mute and block, with lists | yes | implemented | host | | Muted words, hide reposts | yes | implemented: Wolfram's matcher (`wf_muted_words_match`), including mute expiry | host | -| 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 | +| Attach an image when composing | yes | implemented: SELECT or the "Add an image" button in compose opens a picker over `sdmc:/3ds/indigo/images` and one folder down in the camera's `sdmc:/DCIM` (JPEG and PNG under 950 KB), then asks for alt text; works on posts, replies and quotes. The type and size filter, the folder scans and the upload are Wolfram's (`wolfram/attach.h`); the camera folder's scan is `wf_attach_scan_images_tree` (Wolfram v0.39.0, not released yet) | host only: the compose state, the picker over both folders and the layouts (snapshots `compose-image`, `attach-picker`). The upload and a real post have not been run on an emulator or a 3DS, and the DCIM listing has not been seen on a console (#19 stays open for that) | | 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 | 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. | diff --git a/src/app/app.c b/src/app/app.c index eb0e10b..3daf200 100644 --- a/src/app/app.c +++ b/src/app/app.c @@ -20,6 +20,7 @@ indigo_app_init(indigo_app *app) /* The memset above leaves text_scale at 0, which is not a valid scale. */ indigo_settings_defaults(&app->settings); indigo_copy_utf8(app->images_dir, sizeof app->images_dir, INDIGO_IMAGES_DIR); + indigo_copy_utf8(app->camera_dir, sizeof app->camera_dir, INDIGO_CAMERA_DIR); } void diff --git a/src/app/app.h b/src/app/app.h index 95a6cad..6be8704 100644 --- a/src/app/app.h +++ b/src/app/app.h @@ -138,9 +138,10 @@ typedef struct { indigo_profile profile; indigo_notifications notifications; indigo_compose compose; - /* Where the attach picker looks; INDIGO_IMAGES_DIR unless a test points it - * elsewhere. */ + /* Where the attach picker looks; INDIGO_IMAGES_DIR and INDIGO_CAMERA_DIR + * unless a test points them elsewhere. */ char images_dir[INDIGO_IMAGE_PATH_MAX]; + char camera_dir[INDIGO_IMAGE_PATH_MAX]; indigo_search search; /* The image the viewer is showing, and the screen it was opened from. */ indigo_image image; diff --git a/src/app/app_screens.c b/src/app/app_screens.c index 219da73..c87e94c 100644 --- a/src/app/app_screens.c +++ b/src/app/app_screens.c @@ -81,7 +81,7 @@ attach_or_detach(indigo_app *app) return; } indigo_app_push_screen(app); - indigo_menu_build_images(&app->menu, app->images_dir); + indigo_menu_build_images(&app->menu, app->images_dir, app->camera_dir); app->screen = INDIGO_SCREEN_MENU; } @@ -197,7 +197,8 @@ menu_choose(indigo_app *app, unsigned item) indigo_app_go_back(app); /* A path that does not fit is dropped rather than cut short into a * different file. */ - if (snprintf(app->compose.image, sizeof app->compose.image, "%s/%s", app->images_dir, + if (snprintf(app->compose.image, sizeof app->compose.image, "%s/%s", + it->camera ? app->camera_dir : app->images_dir, it->payload) >= (int) sizeof app->compose.image) { app->compose.image[0] = '\0'; break; diff --git a/src/app/social.c b/src/app/social.c index 1c99d9a..3e0e4b1 100644 --- a/src/app/social.c +++ b/src/app/social.c @@ -247,26 +247,40 @@ indigo_menu_build(indigo_menu *m, const indigo_post *post, const char *account) } void -indigo_menu_build_images(indigo_menu *m, const char *dir) +indigo_menu_build_images(indigo_menu *m, const char *images_dir, const char *camera_dir) { char names[INDIGO_MENU_MAX - 1][INDIGO_POST_NAME_MAX]; int too_large = 0; - int n; + int app_n = 0; + int camera_n = 0; memset(m, 0, sizeof *m); indigo_copy_utf8(m->title, sizeof m->title, "Images"); m->picking_image = true; - if (dir) { + if (images_dir) { /* So a first-time user finds the folder to copy pictures into. */ - mkdir(dir, 0777); + mkdir(images_dir, 0777); + app_n = wf_attach_scan_images(images_dir, &names[0][0], sizeof names[0], + INDIGO_MENU_MAX - 1, &too_large); } - n = dir ? wf_attach_scan_images(dir, &names[0][0], sizeof names[0], INDIGO_MENU_MAX - 1, - &too_large) - : 0; - for (int i = 0; i < n; i++) { + /* The camera's folder is read one level down (DCIM//). It is + * never created here: the camera owns it. Its rows go after the app's. */ + if (camera_dir) { + camera_n = wf_attach_scan_images_tree(camera_dir, + &names[0][0] + (size_t) app_n * sizeof names[0], + sizeof names[0], INDIGO_MENU_MAX - 1 - app_n, + &too_large); + } + for (int i = 0; i < app_n + camera_n; i++) { + unsigned before = m->count; + add_item(m, INDIGO_MENU_PICK_IMAGE, names[i], names[i]); + if (m->count > before) { + m->items[before].camera = i >= app_n; + } } - add_item(m, INDIGO_MENU_CLOSE, n > 0 ? "Close" : "No images found - close", ""); + add_item(m, INDIGO_MENU_CLOSE, + app_n + camera_n > 0 ? "Close" : "No images found - close", ""); } bool diff --git a/src/app/social.h b/src/app/social.h index 1c11b63..eb87b11 100644 --- a/src/app/social.h +++ b/src/app/social.h @@ -118,6 +118,9 @@ typedef enum { /* Where images to attach are looked for. The person copies .jpg and .png files * here from a computer. */ #define INDIGO_IMAGES_DIR "sdmc:/3ds/indigo/images" +/* The 3DS camera's folder. It saves photos as DCIM//, so the + * picker also lists the images one folder down here. */ +#define INDIGO_CAMERA_DIR "sdmc:/DCIM" /* A post being written. Reply and quote both refer to `target`; a reply also * needs the thread root so it lands in the right conversation. */ @@ -202,6 +205,9 @@ typedef struct { indigo_menu_kind kind; char label[INDIGO_POST_NAME_MAX * 2]; char payload[INDIGO_FACET_TARGET_MAX]; + /* For an image picker row: the payload is under the camera folder rather + * than the images folder. */ + bool camera; } indigo_menu_item; typedef struct { @@ -221,10 +227,12 @@ typedef struct { /* `post` may be NULL when nothing is selected: the facet targets are then * left out. `account` is the signed-in handle for the profile entry. */ void indigo_menu_build(indigo_menu *m, const indigo_post *post, const char *account); -/* Fill the menu with the postable images in `dir` (see wolfram/attach.h), one - * INDIGO_MENU_PICK_IMAGE item per file whose payload is the file name, and a - * Close item so an empty folder still has a way out. `title` becomes "Images". */ -void indigo_menu_build_images(indigo_menu *m, const char *dir); +/* Fill the menu with the postable images in `images_dir` (see wolfram/attach.h) + * and, one folder down, in `camera_dir`. Each file is an INDIGO_MENU_PICK_IMAGE + * item whose payload is its name under that folder ("folder/name" for the + * camera's subfolders), with `camera` set for the camera rows. A Close item + * follows, so an empty picker still has a way out. `title` becomes "Images". */ +void indigo_menu_build_images(indigo_menu *m, const char *images_dir, const char *camera_dir); /* Move the selection, keeping it inside the `rows` visible rows. */ bool indigo_menu_move(indigo_menu *m, int delta, unsigned rows); /* The item shown in visible row `row`, or NULL when that row is empty. */ diff --git a/src/ui/layout_top.c b/src/ui/layout_top.c index 00c03d4..5fa80d8 100644 --- a/src/ui/layout_top.c +++ b/src/ui/layout_top.c @@ -176,11 +176,12 @@ build_top_menu(const indigo_app *app, indigo_canvas *c) indigo_layout_top_title(c, "Add image", "B Close"); indigo_canvas_text(c, 18, 56, 0.6f, COL_TEXT_SOFT, "Choose a picture from"); indigo_canvas_text(c, 18, 78, 0.6f, COL_TEXT, "%s", INDIGO_IMAGES_DIR); + indigo_canvas_text(c, 18, 100, 0.6f, COL_TEXT, "and %s (camera)", INDIGO_CAMERA_DIR); snprintf(note, sizeof note, "Copy .jpg or .png files there from a computer; each must be under %d KB. " "You can describe the picture for people who cannot see it after choosing it.", WF_ATTACH_MAX_BYTES / 1000); - indigo_layout_top_paragraph(c, 18, 110, 0.55f, COL_TEXT_DIM, 5, note); + indigo_layout_top_paragraph(c, 18, 124, 0.55f, COL_TEXT_DIM, 4, note); return; } indigo_layout_top_title(c, "Menu", "B Close"); diff --git a/tests/tests.c b/tests/tests.c index c4b85db..aaa647e 100644 --- a/tests/tests.c +++ b/tests/tests.c @@ -660,6 +660,110 @@ test_attach_image(void) remove(dir); } +/* The picker also lists the camera's DCIM// files, after the + * app folder's own, and choosing one attaches its full path. */ +static void +test_attach_camera_folder(void) +{ + char camera[] = "build-host/attach-camera"; + char appdir[] = "build-host/attach-camera-app"; + char path[256]; + indigo_app app; + indigo_input in = {0}; + FILE *f; + + mkdir("build-host", 0777); + mkdir(camera, 0777); + mkdir(appdir, 0777); + snprintf(path, sizeof path, "%s/100NIN01", camera); + mkdir(path, 0777); + snprintf(path, sizeof path, "%s/100NIN01/IMG_0002.JPG", camera); + f = fopen(path, "wb"); + if (f) { + fputs("jpg", f); + fclose(f); + } + snprintf(path, sizeof path, "%s/100NIN01/notes.txt", camera); + f = fopen(path, "wb"); + if (f) { + fputs("x", f); + fclose(f); + } + snprintf(path, sizeof path, "%s/top.png", camera); + f = fopen(path, "wb"); + if (f) { + fputs("png", f); + fclose(f); + } + + indigo_app_init(&app); + indigo_copy_utf8(app.images_dir, sizeof app.images_dir, appdir); + indigo_copy_utf8(app.camera_dir, sizeof app.camera_dir, camera); + app.screen = INDIGO_SCREEN_COMPOSE; + app.compose.mode = INDIGO_COMPOSE_POST; + indigo_copy_utf8(app.compose.text, sizeof app.compose.text, "a post with a camera picture"); + + in.refresh = true; + indigo_app_update(&app, &in); + in.refresh = false; + CHECK(app.screen == INDIGO_SCREEN_MENU); + CHECK(app.menu.picking_image); + /* Nothing in the app folder; the camera's picture and top-level picture, + * in name order, then Close. The text file is not an image. */ + CHECK(app.menu.count == 3); + CHECK(strcmp(app.menu.items[0].label, "100NIN01/IMG_0002.JPG") == 0); + CHECK(app.menu.items[0].camera); + CHECK(strcmp(app.menu.items[1].label, "top.png") == 0); + CHECK(app.menu.items[1].camera); + CHECK(app.menu.items[2].kind == INDIGO_MENU_CLOSE); + + /* Choosing the subfolder picture attaches its full path. */ + in.confirm = true; + indigo_app_update(&app, &in); + in.confirm = false; + CHECK(app.screen == INDIGO_SCREEN_COMPOSE); + CHECK(indigo_compose_has_image(&app.compose)); + snprintf(path, sizeof path, "%s/100NIN01/IMG_0002.JPG", camera); + CHECK(strcmp(app.compose.image, path) == 0); + CHECK(indigo_app_peek_request(&app) == INDIGO_REQUEST_EDIT_IMAGE_ALT); + indigo_app_take_request(&app, NULL); + + /* An app-folder picture is still under the app folder. */ + snprintf(path, sizeof path, "%s/own.jpg", appdir); + f = fopen(path, "wb"); + if (f) { + fputs("jpg", f); + fclose(f); + } + indigo_compose_clear_image(&app.compose); + app.screen = INDIGO_SCREEN_COMPOSE; + in.refresh = true; + indigo_app_update(&app, &in); + in.refresh = false; + CHECK(app.screen == INDIGO_SCREEN_MENU); + CHECK(app.menu.count == 4); + CHECK(strcmp(app.menu.items[0].label, "own.jpg") == 0); + CHECK(!app.menu.items[0].camera); + in.confirm = true; + indigo_app_update(&app, &in); + in.confirm = false; + snprintf(path, sizeof path, "%s/own.jpg", appdir); + CHECK(strcmp(app.compose.image, path) == 0); + + snprintf(path, sizeof path, "%s/own.jpg", appdir); + remove(path); + snprintf(path, sizeof path, "%s/100NIN01/IMG_0002.JPG", camera); + remove(path); + snprintf(path, sizeof path, "%s/100NIN01/notes.txt", camera); + remove(path); + snprintf(path, sizeof path, "%s/100NIN01", camera); + remove(path); + snprintf(path, sizeof path, "%s/top.png", camera); + remove(path); + remove(camera); + remove(appdir); +} + static void test_compose_gate_text_fits(void) { @@ -5391,6 +5495,7 @@ main(void) test_new_post_reply_gate(); test_compose_gate_text_fits(); test_attach_image(); + test_attach_camera_folder(); test_store_file(); test_compose_flow(); test_notifications(); diff --git a/tools/snapshot.c b/tools/snapshot.c index 79e2c86..12b99f8 100644 --- a/tools/snapshot.c +++ b/tools/snapshot.c @@ -790,7 +790,19 @@ fill_social(indigo_app *app, const scenario *s) fclose(f); } } - indigo_menu_build_images(&app->menu, "build-host/snapshots/pictures"); + /* And the camera's folder: one picture in a DCIM-style subfolder. */ + mkdir("build-host/snapshots/camera", 0777); + mkdir("build-host/snapshots/camera/100NIN01", 0777); + snprintf(path, sizeof path, "build-host/snapshots/camera/100NIN01/IMG_0042.JPG"); + { + FILE *f = fopen(path, "wb"); + if (f) { + fputs("x", f); + fclose(f); + } + } + indigo_menu_build_images(&app->menu, "build-host/snapshots/pictures", + "build-host/snapshots/camera"); } else { indigo_menu_build(&app->menu, indigo_timeline_selected(&app->timeline), app->signin.account); From cb8599275899691dbe18f9ca13a70ac0430b830f Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Thu, 8 Oct 2026 00:26:26 +0100 Subject: [PATCH 2/3] docs(changelog): link the camera folder picker PR --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac359fb..3e5f4f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ as the release notes. ### Added -- The image picker also lists the pictures the 3DS camera saves in `sdmc:/DCIM`, one folder down (`DCIM//`), after the pictures in `sdmc:/3ds/indigo/images`. Choosing one attaches it from its full path. The picker's top screen names both folders. This needs Wolfram v0.39.0's `wf_attach_scan_images_tree`, which is not released yet, so this PR is blocked on that release. +- The image picker also lists the pictures the 3DS camera saves in `sdmc:/DCIM`, one folder down (`DCIM//`), after the pictures in `sdmc:/3ds/indigo/images`. Choosing one attaches it from its full path. The picker's top screen names both folders. This needs Wolfram v0.39.0's `wf_attach_scan_images_tree`, which is not released yet, so this PR is blocked on that release. ([#74](https://github.com/ewanc26/indigo/pull/74)) ### Changed From a14b2a8aba2edc846ad758f179543e8f3e4fc9f4 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Thu, 8 Oct 2026 00:41:07 +0100 Subject: [PATCH 3/3] build(wolfram): pin v0.39.0 Co-Authored-By: Claude Sonnet 5.5 --- wolfram.ref | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wolfram.ref b/wolfram.ref index 765098d..2302e30 100644 --- a/wolfram.ref +++ b/wolfram.ref @@ -1 +1 @@ -v0.38.0 +v0.39.0