diff --git a/CHANGELOG.md b/CHANGELOG.md index 4774dad..3e5f4f3 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. ([#74](https://github.com/ewanc26/indigo/pull/74)) + ### 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); 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