From 64a240b082885b4f08541f193a5fce6a29eac0b5 Mon Sep 17 00:00:00 2001 From: nino Date: Sat, 24 Oct 2026 13:08:11 -0600 Subject: [PATCH] Viewer: save to Vita, turn with L/R; album name order, cover shape setting - Inside an album, SELECT cycles newest / oldest / file name A-Z / Z-A (natural, case-insensitive; name orders drop the month headers and L/R jump by initial letter). Asset originalFileName is now parsed. - Full-screen viewer: Triangle or the "Save to Vita" button downloads the original (falling back to Immich's full-size / preview JPEG) and exports it to the Photos app via ScePhotoExport (new src/export.c). The video downloader is now a generic download_to_file(). - R / L turn the whole viewer 90 degrees clockwise / counter-clockwise so the Vita can be held upright (new src/viewrot.c): it renders into a portrait render target drawn rotated, and maps d-pad, sticks and touch into the turned screen. Zoom moves to the right stick only. SCREEN_W/H are now variables for that logical screen. - Server & Backup page: album cover shape setting (vertical 3:4, square, panoramic 16:9), saved as covershape= in config.txt. - Test builds: "rup" holds the right stick up in test_input.txt. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 1 + README.md | 16 ++++- src/albums.c | 11 ++- src/api.c | 9 +++ src/app.c | 140 ++++++++++++++++++++++++++++++++---- src/config.c | 6 ++ src/display.c | 110 +++++++++++++++++++++++----- src/export.c | 189 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.c | 11 ++- src/map.c | 2 + src/state.c | 18 +++++ src/ui.c | 14 +++- src/video.c | 50 ++++++++----- src/viewrot.c | 176 +++++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 698 insertions(+), 55 deletions(-) create mode 100644 src/export.c create mode 100644 src/viewrot.c diff --git a/CMakeLists.txt b/CMakeLists.txt index e0ac5b9..23f5f18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ target_link_libraries(${PROJECT_NAME} SceNetCtl_stub SceAvPlayer_stub SceAudio_stub + ScePhotoExport_stub ) # UNSAFE: reading the camera folders (ux0:picture, ux0:video) is outside diff --git a/README.md b/README.md index 8133ad8..7483355 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,18 @@ handheld. | △ | Reload the album list | | O | Clear the search, then back | +Cover shape — **Vertical** (3:4), **Square** or **Panoramic** (16:9) — is +picked on the *Server & Backup* page (**L / R** or tap) and saved as +`covershape=` in `config.txt`. + +**Inside an album** + +| Button | Action | +| --- | --- | +| SELECT | Order: newest → oldest → file name A‑Z → Z‑A (name orders drop the month headers) | +| L / R | Jump a month (or, sorted by name, an initial letter) | +| O | Back to the album list | + **Places map** | Button | Action | @@ -107,8 +119,10 @@ handheld. | Button | Action | | --- | --- | | ← / → | Previous / next | -| Right stick / L · R | Zoom · left stick pans | +| R / L | Turn the viewer 90° clockwise / counter‑clockwise — hold the Vita upright and portrait photos fill the screen (d‑pad, sticks and touch follow the turn) | +| Right stick | Zoom · left stick pans | | X | Play video / retry a failed load | +| △ (or tap "Save to Vita") | Save the photo to the Vita's Photos app (JPEG/PNG original, else Immich's full-size JPEG) | | O | Back | **Video playback** — X pause/resume · ←/→ seek ±10 s · O stop. diff --git a/src/albums.c b/src/albums.c index e9162fe..7f686aa 100644 --- a/src/albums.c +++ b/src/albums.c @@ -23,7 +23,7 @@ #define ALB_COLS 4 #define ALB_CW 210.0f /* card width */ -#define ALB_COVH 158.0f /* cover height (4:3) */ +#define ALB_COVH alb_cover_h() /* cover height: the cover shape */ #define ALB_GAP 20.0f #define ALB_ROWH (ALB_COVH + 62.0f) /* cover + two text lines + gap */ #define ALB_TOP 64.0f /* below the page header */ @@ -38,6 +38,13 @@ #define ALB_OX (ALB_SX + ALB_SW + 14.0f) /* sort chip */ #define ALB_OW 230.0f +/* cover height for the chosen shape: 3:4, 1:1 or 16:9 */ +static float alb_cover_h(void) +{ + return g_cover_shape == COVER_VERTICAL ? 280.0f : + g_cover_shape == COVER_WIDE ? 118.0f : 210.0f; +} + enum { ALB_NONE, ALB_BACK, ALB_OPEN, ALB_REFRESH, ALB_SEARCH }; /* sort orders, cycled by SELECT */ @@ -244,7 +251,7 @@ static void draw_album_card(int a, float x, float y, int selected) draw_sel_outline(x - 5, y - 5, ALB_CW + 10, ALB_COVH + 10); vita2d_draw_rectangle(x, y, ALB_CW, ALB_COVH, card); if (tex) { - /* crop to fill the 4:3 cover box */ + /* crop to fill the cover box */ float tw = vita2d_texture_get_width(tex); float th = vita2d_texture_get_height(tex); float s = fmaxf(ALB_CW / tw, ALB_COVH / th); diff --git a/src/api.c b/src/api.c index 720c06f..bb53bab 100644 --- a/src/api.c +++ b/src/api.c @@ -86,6 +86,7 @@ static int parse_assets(const char *js, size_t jslen, char *err, size_t errlen) added++; g_asset_ids[cur][0] = '\0'; g_asset_dates[cur][0] = '\0'; + g_asset_names[cur][0] = '\0'; g_asset_ratio[cur] = 0.0f; g_asset_rot[cur] = 0; g_asset_is_video[cur] = 0; @@ -136,6 +137,10 @@ static int parse_assets(const char *js, size_t jslen, char *err, size_t errlen) else if (klen == 4 && !strncmp(k, "type", 4)) g_asset_is_video[cur] = (vlen == 5 && !strncmp(js + val->start, "VIDEO", 5)); + else if (klen == 16 && !strncmp(k, "originalFileName", 16)) + /* may be cut mid UTF-8 sequence; only sorted, never drawn */ + snprintf(g_asset_names[cur], sizeof(g_asset_names[0]), + "%.*s", vlen, js + val->start); } if (cur >= 0 && exw > 0 && exh > 0) g_asset_ratio[cur] = (exori >= 5 && exori <= 8) ? @@ -175,6 +180,8 @@ static int dedup_new_assets(int before) sizeof(g_asset_ids[0])); memcpy(g_asset_dates[i], g_asset_dates[n], sizeof(g_asset_dates[0])); + memcpy(g_asset_names[i], g_asset_names[n], + sizeof(g_asset_names[0])); g_asset_ratio[i] = g_asset_ratio[n]; g_asset_rot[i] = g_asset_rot[n]; g_asset_is_video[i] = g_asset_is_video[n]; @@ -642,6 +649,8 @@ static int fetch_album_assets(int a) !strcmp(g_asset_ids[j], g_asset_ids[i])) { memcpy(g_asset_dates[j], g_asset_dates[i], sizeof(g_asset_dates[0])); + memcpy(g_asset_names[j], g_asset_names[i], + sizeof(g_asset_names[0])); g_asset_ratio[j] = g_asset_ratio[i]; g_asset_rot[j] = g_asset_rot[i]; g_asset_is_video[j] = g_asset_is_video[i]; diff --git a/src/app.c b/src/app.c index 03a0589..e5b1d6b 100644 --- a/src/app.c +++ b/src/app.c @@ -21,6 +21,8 @@ static char g_boot_status[48] = "Loading..."; static volatile int g_boot_done; static volatile int g_boot_login_failed; #ifdef VIMM_TEST_INPUT +/* a pseudo button: hold the right stick up (zoom in) */ +#define TEST_RSTICK_UP 0x80000000u /* TEST BUILDS ONLY (-DVIMM_TEST_INPUT): scripted buttons for emulator runs * where synthetic key events can't reach Vita3K. Lines in * ux0:/data/vitaimmich/test_input.txt: "x", "down:3" (repeat), "wait:60" @@ -53,6 +55,7 @@ static unsigned int test_input_buttons(void) { "left", SCE_CTRL_LEFT }, { "right", SCE_CTRL_RIGHT }, { "l", SCE_CTRL_LTRIGGER }, { "r", SCE_CTRL_RTRIGGER }, { "sel", SCE_CTRL_SELECT }, { "start", SCE_CTRL_START }, + { "rup", TEST_RSTICK_UP }, }; if (!strcmp(name, "wait")) { queue_btn[qn] = 0; queue_len[qn++] = n; @@ -238,6 +241,8 @@ int main(void) /* on-screen keyboard (smart search). the IME runs as a common dialog; * the config tells it the system language + enter/cancel button map */ sceSysmoduleLoadModule(SCE_SYSMODULE_IME); + /* Save to Vita: copies photos into the Photos app */ + sceSysmoduleLoadModule(SCE_SYSMODULE_PHOTO_EXPORT); { SceCommonDialogConfigParam cfg; sceCommonDialogConfigParamInit(&cfg); @@ -416,10 +421,15 @@ int main(void) int pf_pending = -1; /* slot being fetched right now, -1 none */ for (;;) { + view_leave(); /* only the photo viewer may be turned */ SceCtrlData pad; sceCtrlPeekBufferPositive(0, &pad, 1); #ifdef VIMM_TEST_INPUT pad.buttons |= test_input_buttons(); + if (pad.buttons & TEST_RSTICK_UP) { + pad.buttons &= ~TEST_RSTICK_UP; + pad.ry = 0; + } #endif unsigned int pressed = pad.buttons & ~prev_buttons; prev_buttons = pad.buttons; @@ -476,6 +486,22 @@ int main(void) if (held_frames > 18 && held_frames % 5 == 0) nav |= pad.buttons & dirs; + /* SELECT inside an album cycles its order (date / name); the + * grid restarts at the top of the new order */ + if ((pressed & SCE_CTRL_SELECT) && mode == MODE_GRID && + g_album_active) { + g_album_sort = (g_album_sort + 1) % ASORT_COUNT; + rebuild_display(); + log_line("album sort: %s (first: %s, %s)", + g_album_sort_label[g_album_sort], + g_disp_count > 0 ? g_asset_names[g_disp[0].idx] : "-", + g_disp_count > 1 ? g_asset_names[g_disp[1].idx] : "-"); + sel = 0; + scroll = target = 0; + grid_last_sel = -1; + continue; + } + /* SELECT opens the cloud / backup page from the grid or detail */ if ((pressed & SCE_CTRL_SELECT) && (mode == MODE_GRID || mode == MODE_DETAIL)) { @@ -1105,13 +1131,25 @@ int main(void) int is_local = (it.src == SRC_LOCAL); int is_video = disp_is_video(sel); - /* zoom with the triggers or the right stick (push up to - * zoom in, down to zoom out), continuous while held */ + /* R / L turn the viewer 90 degrees clockwise / counter- + * clockwise (hold the Vita upright for portrait photos); + * from here on the frame works in the turned screen: + * SCREEN_W/H, d-pad, sticks and touch all follow it */ + if (!slide_anim && !touch_active && + (pressed & (SCE_CTRL_RTRIGGER | SCE_CTRL_LTRIGGER))) { + view_turn((pressed & SCE_CTRL_RTRIGGER) ? 90 : -90); + zoom = 1.0f; + panx = pany = 0.0f; + } + view_enter(); + nav = view_buttons(nav); + pad.buttons = view_buttons(pad.buttons); + view_stick(&pad.lx, &pad.ly); + view_stick(&pad.rx, &pad.ry); + + /* zoom with the right stick (push up to zoom in, down + * to zoom out), continuous while held */ if (!is_video && detail_tex) { - if (pad.buttons & SCE_CTRL_RTRIGGER) - zoom *= 1.04f; - if (pad.buttons & SCE_CTRL_LTRIGGER) - zoom /= 1.04f; float rz = (128 - pad.ry) / 128.0f; /* up positive */ if (rz > 0.18f || rz < -0.18f) zoom *= 1.0f + rz * 0.05f; @@ -1123,6 +1161,10 @@ int main(void) } } int zoomed = (zoom > 1.001f); + /* Triangle (or the button) saves a server photo to the + * Photos app; local photos already live on the Vita */ + int can_save = !is_local && !is_video; + int do_save = can_save && (pressed & SCE_CTRL_TRIANGLE); if (zoomed) { /* pan with the left stick + d-pad; don't browse */ @@ -1159,6 +1201,7 @@ int main(void) if (td.reportNum > 0) { float tx = td.report[0].x * 0.5f; float ty = td.report[0].y * 0.5f; + view_touch(&tx, &ty); if (!touch_active) { touch_active = 1; touch_dragged = 0; @@ -1201,9 +1244,14 @@ int main(void) else slide_goal = 0; /* spring back */ slide_anim = 1; + } else if (!touch_dragged && can_save && + !zoomed && !slide_anim && + save_button_hit(touch_x, touch_y)) { + do_save = 1; } else if (!touch_dragged && is_video && !slide_anim) { /* tap on the poster plays it */ + view_leave(); g_dl_bg = detail_tex ? detail_tex : disp_thumb(sel); @@ -1261,6 +1309,7 @@ int main(void) if (pressed & SCE_CTRL_CROSS) { if (is_video) { /* play even if the poster fetch failed */ + view_leave(); g_dl_bg = detail_tex ? detail_tex : disp_thumb(sel); g_video_rot = 0; @@ -1282,6 +1331,20 @@ int main(void) if (detail_failed) detail_idx = -1; /* retry */ } + if (do_save && !slide_anim) { + view_leave(); + g_dl_bg = detail_tex ? detail_tex : disp_thumb(sel); + save_photo_to_vita(it.idx); + g_dl_bg = NULL; + /* buttons used on the progress screen (O cancels) + * must not also act here */ + do { + sceCtrlPeekBufferPositive(0, &pad, 1); + sceKernelDelayThread(10 * 1000); + } while (pad.buttons); + prev_buttons = 0; + continue; + } /* left/right may have moved the selection; refresh the * item we load + draw below (the X handler above acted @@ -1289,6 +1352,7 @@ int main(void) it = g_disp[sel]; is_local = (it.src == SRC_LOCAL); is_video = disp_is_video(sel); + can_save = !is_local && !is_video; /* collect a finished neighbor prefetch (and drop it * if browsing already moved past its neighborhood) */ @@ -1454,8 +1518,7 @@ int main(void) } pf_skip: - vita2d_start_drawing(); - vita2d_clear_screen(); + view_begin_drawing(); int sliding = (slide_anim || slide_x > 0.5f || slide_x < -0.5f); @@ -1531,23 +1594,25 @@ pf_skip: char hud[200]; if (zoom > 1.001f) snprintf(hud, sizeof(hud), - "%d / %d %.0f%% stick/d-pad pan L/R zoom O back", + "%d / %d %.0f%% stick/d-pad pan R-stick zoom O back", sel + 1, g_disp_count, zoom * 100.0f); else snprintf(hud, sizeof(hud), - "%d / %d %.19s < > browse L/R zoom %sO back", + "%d / %d %.19s < > browse L/R turn %sO back", sel + 1, g_disp_count, g_disp_count > 0 ? disp_date(sel) : "", xhint); draw_hud(hud); + if (can_save && !zoomed && !sliding) + draw_save_button(); + draw_toast(); /* throbber centred over the preview while the full-res * image is still loading */ if (detail_loading && disp_thumb(sel) && !zoomed && !sliding) draw_throbber(SCREEN_W / 2.0f, SCREEN_H / 2.0f, 24.0f, frame); - vita2d_end_drawing(); - vita2d_swap_buffers(); + view_end_drawing(); } else if (mode == MODE_CLOUD) { /* server + backup overview, opened from the cloud * button on the search bar */ @@ -1593,6 +1658,14 @@ pf_skip: int do_toggle = (pressed & SCE_CTRL_SQUARE) != 0; int do_back = (pressed & SCE_CTRL_CIRCLE) != 0; int do_logout = 0; + /* album cover shape picker (right column): three + * segments, tap one or step with L / R */ + float shx = 584, shy = 306, shw = 120, shh = 40; + int new_shape = g_cover_shape; + if (pressed & SCE_CTRL_LTRIGGER) + new_shape = (new_shape + COVER_COUNT - 1) % COVER_COUNT; + if (pressed & SCE_CTRL_RTRIGGER) + new_shape = (new_shape + 1) % COVER_COUNT; /* O / Back button in the top-right corner */ float bkd = 36, bkcx = SCREEN_W - 30, bkcy = 36; /* Log out button in the top-left corner */ @@ -1628,6 +1701,10 @@ pf_skip: touch_y >= loy && touch_y <= loy + loh) { do_logout = 1; /* top-left Log out */ + } else if (touch_y >= shy && touch_y <= shy + shh && + touch_x >= shx && + touch_x < shx + COVER_COUNT * shw) { + new_shape = (int)((touch_x - shx) / shw); } else if (touch_y >= tgy - 6 && touch_y <= tgy + tgh + 6 && touch_x >= 60 && touch_x <= tgx + tgw) { @@ -1690,6 +1767,12 @@ pf_skip: log_line("cloud: auto-upload %s", g_autobackup ? "on" : "off"); } + if (new_shape != g_cover_shape) { + g_cover_shape = new_shape; + save_config(); + log_line("cloud: album covers %s", + g_cover_shape_key[g_cover_shape]); + } if (do_upload) { int q = queue_all_uploads(); log_line("cloud: queued %d uploads", q); @@ -1793,6 +1876,39 @@ pf_skip: draw_ps_button(tgx - 23, tgy + tgh / 2, 38, ICON_SQUARE, RGBA8(232, 120, 175, 255), 31); + /* album cover shape: segmented control, the chosen + * shape highlighted, each with a small shape preview */ + draw_text(shx, shy - 14, RGBA8(255, 255, 255, 255), 1.0f, + "Album covers"); + for (int k = 0; k < COVER_COUNT; k++) { + float sx = shx + k * shw; + int on = (k == g_cover_shape); + vita2d_texture *seg = rounded_mask_tex((int)shw - 6, + (int)shh, 10.0f); + uint32_t sc = on ? RGBA8(94, 110, 215, 255) + : RGBA8(46, 46, 52, 255); + if (seg) + vita2d_draw_texture_tint(seg, sx, shy, sc); + else + vita2d_draw_rectangle(sx, shy, shw - 6, shh, sc); + /* the shape itself, 20 px tall at most */ + float pw = k == COVER_VERTICAL ? 15 : + k == COVER_SQUARE ? 20 : 26; + float ph = k == COVER_WIDE ? 15 : 20; + vita2d_draw_rectangle(sx + 8, shy + (shh - ph) / 2, + pw, ph, + on ? RGBA8(235, 235, 245, 255) + : RGBA8(130, 130, 138, 255)); + draw_text(sx + 38, shy + shh - 14, + on ? RGBA8(245, 245, 250, 255) : dim, 0.7f, + g_cover_shape_label[k]); + } + { + const char *h = "L / R"; + draw_text(shx + COVER_COUNT * shw - 6 - + text_width(0.75f, h), shy - 14, dim, 0.75f, h); + } + /* See details + Upload all buttons */ vita2d_texture *db = rounded_mask_tex((int)dbw, (int)bth, 10.0f); if (db) diff --git a/src/config.c b/src/config.c index cfb8c9c..f1a8b12 100644 --- a/src/config.c +++ b/src/config.c @@ -86,6 +86,12 @@ static int load_config(void) } } else if (!strncmp(s, "syncmaxmb=", 10)) g_syncmax_mb = atol(clean_line(s + 10)); + else if (!strncmp(s, "covershape=", 11)) { + const char *v = clean_line(s + 11); + for (int k = 0; k < COVER_COUNT; k++) + if (!strcmp(v, g_cover_shape_key[k])) + g_cover_shape = k; + } if (!eolsave) break; lp = eol + 1; diff --git a/src/display.c b/src/display.c index bc506c2..321f8c1 100644 --- a/src/display.c +++ b/src/display.c @@ -65,6 +65,74 @@ static int disp_cmp(const void *a, const void *b) return strcmp(dy, dx); /* newest first */ } +/* the open album, sorted by name: one flat grid without month headers */ +static int layout_flat(void) +{ + return g_album_active && g_album_sort >= ASORT_NAME_AZ; +} + +static int disp_cmp_old(const void *a, const void *b) +{ + return -disp_cmp(a, b); /* oldest first */ +} + +/* case-insensitive "natural" order: digit runs compare by value, so + * IMG_2 sorts before IMG_10 */ +static int name_cmp(const char *a, const char *b) +{ + while (*a && *b) { + if (isdigit((unsigned char)*a) && isdigit((unsigned char)*b)) { + while (*a == '0') + a++; + while (*b == '0') + b++; + const char *ea = a, *eb = b; + while (isdigit((unsigned char)*ea)) + ea++; + while (isdigit((unsigned char)*eb)) + eb++; + if (ea - a != eb - b) + return (int)(ea - a) - (int)(eb - b); + int r = strncmp(a, b, ea - a); + if (r) + return r; + a = ea; + b = eb; + continue; + } + int ca = tolower((unsigned char)*a), cb = tolower((unsigned char)*b); + if (ca != cb) + return ca - cb; + a++; + b++; + } + return (unsigned char)*a - (unsigned char)*b; +} + +static int disp_cmp_name(const void *a, const void *b) +{ + const struct disp_item *x = a, *y = b; + int r = name_cmp(g_asset_names[x->idx], g_asset_names[y->idx]); + return r ? r : disp_cmp(a, b); +} + +static int disp_cmp_name_desc(const void *a, const void *b) +{ + return -disp_cmp_name(a, b); +} + +/* group of a slot: its "YYYY-MM" month, or in a name-sorted album the + * file name's first letter (what L/R jumps between) */ +static void disp_group(int d, char *out, size_t len) +{ + if (layout_flat()) { + const char *n = g_asset_names[g_disp[d].idx]; + snprintf(out, len, "%c", toupper((unsigned char)n[0])); + } else { + snprintf(out, len, "%.7s", disp_date(d)); + } +} + /* turn a sortable "YYYY-MM-..." date into a "Month YYYY" section title */ static void month_label(const char *date, char *out, size_t len) { @@ -133,12 +201,13 @@ static void layout_grid(void) int row_start = 0; char curkey[8] = ""; + int flat = layout_flat(); for (int d = 0; d < g_disp_count; d++) { const char *date = disp_date(d); char key[8]; - snprintf(key, sizeof(key), "%.7s", date); /* YYYY-MM */ + snprintf(key, sizeof(key), "%.7s", flat ? "" : date); /* YYYY-MM */ - if (strcmp(key, curkey) != 0) { + if (!flat && strcmp(key, curkey) != 0) { if (d > row_start) /* month's partial last row */ layout_finish_row(row_start, d, &y, natw, 0); row_start = d; @@ -200,7 +269,8 @@ static void rebuild_display(void) } /* album mode: the open album's members (server assets), date-grouped - * like search results; placeholders without a date are skipped */ + * like search results or in a flat name order (g_album_sort); + * placeholders without a date are skipped */ if (g_album_active) { if (!grow_disp(g_album_idx_count > 0 ? g_album_idx_count : 1)) return; @@ -214,7 +284,12 @@ static void rebuild_display(void) n++; } g_disp_count = n; - qsort(g_disp, n, sizeof(g_disp[0]), disp_cmp); + int (*cmp)(const void *, const void *) = + g_album_sort == ASORT_DATE_OLD ? disp_cmp_old : + g_album_sort == ASORT_NAME_AZ ? disp_cmp_name : + g_album_sort == ASORT_NAME_ZA ? disp_cmp_name_desc : + disp_cmp; + qsort(g_disp, n, sizeof(g_disp[0]), cmp); layout_grid(); return; } @@ -297,9 +372,9 @@ static void rebuild_keep_view(int *sel, float *scroll, float *target) *sel = 0; } -/* first item of the month-run after (dir>0, older) or before (dir<0, newer) - * the one containing display slot d; the timeline is "YYYY-MM"-grouped and - * date-descending, so a month is a contiguous run of equal 7-char prefixes */ +/* first item of the group-run after (dir>0) or before (dir<0) the one + * containing display slot d; the grid is sorted, so a month (or, in a + * name-sorted album, an initial letter) is a contiguous run (disp_group) */ static int month_jump(int d, int dir) { if (g_disp_count == 0) @@ -308,25 +383,28 @@ static int month_jump(int d, int dir) d = 0; if (d >= g_disp_count) d = g_disp_count - 1; - const char *cur = disp_date(d); + char cur[8], k[8]; + disp_group(d, cur, sizeof(cur)); int i = d; if (dir > 0) { while (i < g_disp_count - 1) { i++; - if (strncmp(disp_date(i), cur, 7)) - return i; /* first item of the next month */ + disp_group(i, k, sizeof(k)); + if (strcmp(k, cur)) + return i; /* first item of the next group */ } - return g_disp_count - 1; /* already in the last month */ + return g_disp_count - 1; /* already in the last group */ } - /* back to the start of the current month-run */ - while (i > 0 && !strncmp(disp_date(i - 1), cur, 7)) + /* back to the start of the current run */ + while (i > 0 && (disp_group(i - 1, k, sizeof(k)), !strcmp(k, cur))) i--; if (i == 0) return 0; - /* then to the start of the previous (newer) month-run */ - const char *prev = disp_date(i - 1); + /* then to the start of the previous run */ + char prev[8]; + disp_group(i - 1, prev, sizeof(prev)); i--; - while (i > 0 && !strncmp(disp_date(i - 1), prev, 7)) + while (i > 0 && (disp_group(i - 1, k, sizeof(k)), !strcmp(k, prev))) i--; return i; } diff --git a/src/export.c b/src/export.c new file mode 100644 index 0000000..db27d5d --- /dev/null +++ b/src/export.c @@ -0,0 +1,189 @@ +/* vitaImmich unity build — part of one translation unit. + * This file is #included by src/main.c (in order) and shares all + * file-scope state with the other parts; it is NOT compiled on its own. + * Only src/main.c is listed in CMakeLists.txt. Editors/clangd may flag + * undefined symbols here because of that — those are expected. + */ + +/* ------------------------------------------------------------------ */ +/* save a server photo to the Vita's Photos app */ +/* */ +/* Triangle in the full-screen viewer. The original is downloaded and */ +/* handed to ScePhotoExport, which copies it into ux0:picture and */ +/* registers it with the Photos app. The Vita only takes JPEG/PNG, so */ +/* an original in another format (HEIC, RAW, WebP...) — or one the */ +/* exporter rejects — falls back to Immich's full-size JPEG, then to */ +/* the preview. */ +/* ------------------------------------------------------------------ */ + +#define EXPORT_TMP_PATH CONFIG_DIR "/export.tmp" +#define EXPORT_WORK_SIZE (4 * 1024 * 1024) + +/* a short status line drawn over the viewer for a few seconds */ +static char g_toast[200]; +static uint64_t g_toast_until; + +static void show_toast(const char *text) +{ + snprintf(g_toast, sizeof(g_toast), "%s", text); + g_toast_until = sceKernelGetProcessTimeWide() + 3 * 1000 * 1000; +} + +static void draw_toast(void) +{ + if (!g_toast[0] || sceKernelGetProcessTimeWide() > g_toast_until) + return; + int w = text_width(0.9f, g_toast) + 40; + float x = (SCREEN_W - w) / 2.0f, y = SCREEN_H - 78; + vita2d_texture *pill = rounded_mask_tex(w, 40, 12.0f); + if (pill) + vita2d_draw_texture_tint(pill, x, y, RGBA8(20, 20, 24, 225)); + else + vita2d_draw_rectangle(x, y, w, 40, RGBA8(20, 20, 24, 225)); + draw_text(x + 20, y + 27, RGBA8(240, 240, 245, 255), 0.9f, g_toast); +} + +/* extension of a JPEG/PNG file by its magic bytes, NULL for anything else */ +static const char *image_ext(const char *path) +{ + unsigned char m[4] = { 0 }; + FILE *f = fopen(path, "rb"); + if (!f) + return NULL; + size_t n = fread(m, 1, sizeof(m), f); + fclose(f); + if (n >= 3 && m[0] == 0xFF && m[1] == 0xD8 && m[2] == 0xFF) + return "jpg"; + if (n >= 4 && !memcmp(m, "\x89PNG", 4)) + return "png"; + return NULL; +} + +/* UTF-8 file name (extension dropped) as the UTF-32 photo title */ +static void export_title(const char *name, SceWChar32 *out, int cap) +{ + const char *dot = strrchr(name, '.'); + const char *end = dot && dot != name ? dot : name + strlen(name); + int n = 0; + while (name < end && n < cap - 1) { + unsigned char c = (unsigned char)*name; + unsigned int cp = '?'; + int len = 1; + if (c < 0x80) + cp = c; + else if ((c >> 5) == 0x6 && name + 1 < end) + cp = ((c & 0x1F) << 6) | (name[1] & 0x3F), len = 2; + else if ((c >> 4) == 0xE && name + 2 < end) + cp = ((c & 0x0F) << 12) | ((name[1] & 0x3F) << 6) | + (name[2] & 0x3F), len = 3; + else if ((c >> 3) == 0x1E && name + 3 < end) + cp = ((c & 0x07) << 18) | ((name[1] & 0x3F) << 12) | + ((name[2] & 0x3F) << 6) | (name[3] & 0x3F), len = 4; + out[n++] = cp; + name += len; + } + out[n] = 0; +} + +/* save server asset idx to the Photos app; reports through the toast */ +static void save_photo_to_vita(int idx) +{ + if (g_asset_is_video[idx]) { + show_toast("Only photos can be saved to the Vita"); + return; + } + static void *work; + if (!work) + work = memalign(64, EXPORT_WORK_SIZE); + if (!work) { + show_toast("Not enough memory to save the photo"); + return; + } + + static const char *const sources[] = { + "original", "thumbnail?size=fullsize", "thumbnail?size=preview", + }; + static SceWChar32 title[64]; + export_title(g_asset_names[idx][0] ? g_asset_names[idx] : "Immich", + title, 64); + g_dl_what = "photo"; + char err[160] = "", file[96], out[256]; + int saved = 0; + for (unsigned s = 0; s < sizeof(sources) / sizeof(sources[0]) && !saved; s++) { + char url[700]; + snprintf(url, sizeof(url), "%s/api/assets/%s/%s", g_server, + g_asset_ids[idx], sources[s]); + int r = download_to_file(url, EXPORT_TMP_PATH, err, sizeof(err)); + if (r == 1) { + show_toast("Save cancelled"); + return; + } + if (r < 0) + continue; + const char *ext = image_ext(EXPORT_TMP_PATH); + if (!ext) { + log_line("export %s: %s is not JPEG/PNG", g_asset_ids[idx], + sources[s]); + snprintf(err, sizeof(err), "unsupported image format"); + sceIoRemove(EXPORT_TMP_PATH); + continue; + } + /* the exporter goes by the extension */ + snprintf(file, sizeof(file), CONFIG_DIR "/export.%s", ext); + sceIoRemove(file); + sceIoRename(EXPORT_TMP_PATH, file); + + /* the SDK doesn't document `version`; try the firmware's 3.15 + * value, then 0, and log each result for diagnosis */ + static const int versions[] = { 0x03150021, 0 }; + int ret = -1; + for (unsigned v = 0; v < 2 && ret < 0; v++) { + PhotoExportParam param; + memset(¶m, 0, sizeof(param)); + param.version = versions[v]; + param.photoTitle = title; + out[0] = '\0'; + ret = scePhotoExportFromFile(file, ¶m, work, NULL, NULL, + out, sizeof(out)); + log_line("export %s (%s, v%08X): 0x%08X -> %s", + g_asset_ids[idx], sources[s], versions[v], ret, + out); + } + sceIoRemove(file); + if (ret >= 0) + saved = 1; + else + snprintf(err, sizeof(err), "export error 0x%08X", ret); + } + if (saved) { + show_toast("Saved to the Photos app"); + } else { + char msg[200]; + snprintf(msg, sizeof(msg), "Could not save the photo (%s)", err); + show_toast(msg); + } +} + +/* the viewer's "Save to Vita" button (bottom right; Triangle or a tap) */ +#define SAVE_BTN_W 176.0f +#define SAVE_BTN_H 40.0f +#define SAVE_BTN_X (SCREEN_W - SAVE_BTN_W - 16.0f) +#define SAVE_BTN_Y (SCREEN_H - SAVE_BTN_H - 16.0f) + +static void draw_save_button(void) +{ + vita2d_texture *bg = rounded_mask_tex((int)SAVE_BTN_W, (int)SAVE_BTN_H, 12.0f); + if (bg) + vita2d_draw_texture_tint(bg, SAVE_BTN_X, SAVE_BTN_Y, + RGBA8(20, 20, 24, 200)); + draw_ps_button(SAVE_BTN_X + 22, SAVE_BTN_Y + SAVE_BTN_H / 2, 28, ICON_TRI, + RGBA8(90, 200, 170, 255), 18); + draw_text(SAVE_BTN_X + 44, SAVE_BTN_Y + 27, RGBA8(235, 235, 240, 255), + 0.85f, "Save to Vita"); +} + +static int save_button_hit(float x, float y) +{ + return x >= SAVE_BTN_X && x < SAVE_BTN_X + SAVE_BTN_W && + y >= SAVE_BTN_Y && y < SAVE_BTN_Y + SAVE_BTN_H; +} diff --git a/src/main.c b/src/main.c index a2b9ad2..0a74cba 100644 --- a/src/main.c +++ b/src/main.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -54,8 +55,11 @@ #define JSMN_PARENT_LINKS #include "jsmn.h" -#define SCREEN_W 960 -#define SCREEN_H 544 +/* the logical screen the UI lays itself out in: 960x544, except while the + * rotated photo viewer draws into its portrait render target (544x960) */ +static int g_screen_w = 960, g_screen_h = 544; +#define SCREEN_W g_screen_w +#define SCREEN_H g_screen_h #define COLS 5 /* rough items per row, for prefetch distances only */ #define ROW_H 200 /* justified-grid row height; widths follow aspect */ @@ -124,6 +128,7 @@ static vita2d_texture *g_ic_both; * buffers), so a realloc here can't dangle a pointer under it. */ static char (*g_asset_ids)[40]; static char (*g_asset_dates)[DATELEN]; /* YYYY-MM-DDTHH:MM:SS */ +static char (*g_asset_names)[64]; /* originalFileName, "" = unknown */ static float *g_asset_ratio; /* display w/h from exif, 0 = unknown */ static unsigned char *g_asset_rot; /* raw exif orientation (0 = none) */ static unsigned char *g_asset_is_video; @@ -162,5 +167,7 @@ static char (*g_tex_err)[160]; #include "map.c" /* geotag map: markers, clustering, projection */ #include "albums.c" /* album list page + album open/fetch threads */ #include "video.c" /* video download + SceAvPlayer playback */ +#include "export.c" /* save a photo to the Vita's Photos app */ +#include "viewrot.c" /* L/R-turned photo viewer (render target) */ #include "sync.c" /* local scan/hash/check/upload + sync overview */ #include "app.c" /* main(): init, event loop, threads */ diff --git a/src/map.c b/src/map.c index d8854af..b3f2f66 100644 --- a/src/map.c +++ b/src/map.c @@ -373,6 +373,7 @@ static int map_open_marker(int marker_idx, int *out_sel) idx = g_asset_count++; snprintf(g_asset_ids[idx], sizeof(g_asset_ids[0]), "%s", id); snprintf(g_asset_dates[idx], sizeof(g_asset_dates[0]), "%s", dval); + g_asset_names[idx][0] = '\0'; g_asset_ratio[idx] = 0.0f; /* unknown: detail view fits anyway */ g_asset_rot[idx] = 0; g_asset_is_video[idx] = !strncmp(tval, "VIDEO", 5); @@ -722,6 +723,7 @@ static int asset_for_id(const char *id) int idx = g_asset_count++; snprintf(g_asset_ids[idx], sizeof(g_asset_ids[0]), "%s", id); g_asset_dates[idx][0] = '\0'; /* "" = unresolved; meta_worker fills it */ + g_asset_names[idx][0] = '\0'; g_asset_ratio[idx] = 0.0f; g_asset_rot[idx] = 0; g_asset_is_video[idx] = 0; diff --git a/src/state.c b/src/state.c index c0e1747..e42af93 100644 --- a/src/state.c +++ b/src/state.c @@ -149,6 +149,23 @@ static volatile int g_album_fetching; static volatile int g_album_list_dirty; /* list (re)fetched: rebuild the view */ static int g_album_active; /* grid is showing one album's assets */ +/* order of the open album's grid, cycled by SELECT: date newest / oldest + * (month headers) or file name A-Z / Z-A (one flat grid, no headers) */ +enum { ASORT_DATE_NEW, ASORT_DATE_OLD, ASORT_NAME_AZ, ASORT_NAME_ZA, ASORT_COUNT }; +static int g_album_sort; +static const char *const g_album_sort_label[ASORT_COUNT] = { + "Newest first", "Oldest first", "Name A-Z", "Name Z-A", +}; +/* shape of the album page's cover thumbnails (setting on the Server & + * Backup page, saved as covershape= in config.txt) */ +enum { COVER_VERTICAL, COVER_SQUARE, COVER_WIDE, COVER_COUNT }; +static int g_cover_shape = COVER_SQUARE; +static const char *const g_cover_shape_key[COVER_COUNT] = { + "vertical", "square", "wide", +}; +static const char *const g_cover_shape_label[COVER_COUNT] = { + "Vertical", "Square", "Panoramic", +}; static int g_album_open = -1; /* index into g_album_* being shown */ static int *g_album_idx; /* g_asset indices of its members */ static int g_album_idx_count; @@ -220,6 +237,7 @@ static int grow_assets(int need) return 1; if (!GROW(g_asset_ids, c, need) || !GROW(g_asset_dates, c, need) || + !GROW(g_asset_names, c, need) || !GROW(g_asset_ratio, c, need) || !GROW(g_asset_rot, c, need) || !GROW(g_asset_is_video, c, need) || diff --git a/src/ui.c b/src/ui.c index 0776f8e..933f5dc 100644 --- a/src/ui.c +++ b/src/ui.c @@ -66,7 +66,7 @@ static void draw_sel_outline(float x, float y, float w, float h) * texture's bilinear sampling are what soften the corners. */ static vita2d_texture *rounded_mask_tex(int w, int h, float r) { - enum { NSLOT = 20 }; /* a small fixed set of shapes (pill, buttons, ...) */ + enum { NSLOT = 32 }; /* a small fixed set of shapes (pill, buttons, ...) */ static vita2d_texture *cache[NSLOT]; static int cw[NSLOT], ch[NSLOT]; static float cr[NSLOT]; @@ -473,11 +473,17 @@ static void draw_search_bar(float yoff, int focus) float tx = BAR_PX + 44, ty = by + BAR_H - 13; if (g_album_active && g_album_open >= 0 && g_album_open < g_album_len) { - /* inside an album: its name, and the O chip to go back */ + /* inside an album: its name, the order (SELECT cycles it) and + * the O chip to go back */ char q[80]; - snprintf(q, sizeof(q), "%.48s", g_album_name[g_album_open][0] ? + snprintf(q, sizeof(q), "%.40s", g_album_name[g_album_open][0] ? g_album_name[g_album_open] : "(untitled album)"); draw_text(tx, ty, RGBA8(235, 235, 240, 255), 0.95f, q); + char srt[40]; + snprintf(srt, sizeof(srt), "SELECT %s", + g_album_sort_label[g_album_sort]); + draw_text(BAR_CLEAR_CX - 24 - text_width(0.8f, srt), ty, + RGBA8(140, 140, 148, 255), 0.8f, srt); int gs = 26; vita2d_texture *og = icon_tex(ICON_CIRCLE, gs); if (og) @@ -622,6 +628,8 @@ static void save_config(void) fprintf(f, "syncdir=%s\n", g_syncdirs[i]); if (g_syncmax_mb != 512) fprintf(f, "syncmaxmb=%ld\n", g_syncmax_mb); + if (g_cover_shape != COVER_SQUARE) + fprintf(f, "covershape=%s\n", g_cover_shape_key[g_cover_shape]); fclose(f); } diff --git a/src/video.c b/src/video.c index 53edd6e..38656a1 100644 --- a/src/video.c +++ b/src/video.c @@ -52,6 +52,8 @@ static size_t file_write_cb(void *ptr, size_t size, size_t nmemb, void *ud) } static uint64_t g_dl_last_draw; +/* what the progress screen says is downloading ("video", "photo") */ +static const char *g_dl_what = "video"; /* poster/thumb drawn behind the download progress and the player's startup * (the seconds AVPlayer spends parsing before the first frame); set by the * play call sites, valid for the whole modal download + playback */ @@ -121,11 +123,12 @@ static int dl_progress_cb(void *ud, curl_off_t dltotal, curl_off_t dlnow, char buf[128]; if (dltotal > 0) - snprintf(buf, sizeof(buf), "Downloading video... %.1f / %.1f MB", - dlnow / (1024.0 * 1024.0), dltotal / (1024.0 * 1024.0)); + snprintf(buf, sizeof(buf), "Downloading %s... %.1f / %.1f MB", + g_dl_what, dlnow / (1024.0 * 1024.0), + dltotal / (1024.0 * 1024.0)); else - snprintf(buf, sizeof(buf), "Downloading video... %.1f MB", - dlnow / (1024.0 * 1024.0)); + snprintf(buf, sizeof(buf), "Downloading %s... %.1f MB", + g_dl_what, dlnow / (1024.0 * 1024.0)); vita2d_start_drawing(); vita2d_clear_screen(); @@ -149,17 +152,15 @@ static int dl_progress_cb(void *ud, curl_off_t dltotal, curl_off_t dlnow, return 0; } -/* download the playback stream of asset idx to VIDEO_TMP_PATH. - * returns 0 on success, 1 if the user cancelled, -1 on error. */ -static int download_video(int idx, char *err, size_t errlen) +/* download `url` (an authenticated server path) to the file `path`, with + * the progress screen (O cancels). returns 0 on success, 1 if the user + * cancelled, -1 on error (`err` filled); the file is removed unless 0. */ +static int download_to_file(const char *url, const char *path, char *err, + size_t errlen) { - char url[700]; - snprintf(url, sizeof(url), "%s/api/assets/%s/video/playback", - g_server, g_asset_ids[idx]); - - FILE *f = fopen(VIDEO_TMP_PATH, "wb"); + FILE *f = fopen(path, "wb"); if (!f) { - snprintf(err, errlen, "cannot create %s", VIDEO_TMP_PATH); + snprintf(err, errlen, "cannot create %s", path); return -1; } @@ -180,7 +181,7 @@ static int download_video(int idx, char *err, size_t errlen) curl_easy_setopt(curl, CURLOPT_WRITEDATA, f); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); - /* no overall timeout (videos can be big); abort on a 30 s stall */ + /* no overall timeout (files can be big); abort on a 30 s stall */ curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 30L); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); @@ -200,30 +201,41 @@ static int download_video(int idx, char *err, size_t errlen) if (res == CURLE_OK) curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD_T, &dl); - log_line("video dl %s: res=%d code=%ld bytes=%lld", - g_asset_ids[idx], res, code, (long long)dl); + log_line("%s dl %s: res=%d code=%ld bytes=%lld", g_dl_what, url, res, + code, (long long)dl); curl_slist_free_all(hdrs); curl_easy_cleanup(curl); fclose(f); if (res == CURLE_ABORTED_BY_CALLBACK) { - sceIoRemove(VIDEO_TMP_PATH); + sceIoRemove(path); return 1; } if (res != CURLE_OK) { snprintf(err, errlen, "curl error %d: %s", res, curl_easy_strerror(res)); - sceIoRemove(VIDEO_TMP_PATH); + sceIoRemove(path); return -1; } if (code < 200 || code >= 300) { snprintf(err, errlen, "HTTP %ld", code); - sceIoRemove(VIDEO_TMP_PATH); + sceIoRemove(path); return -1; } return 0; } +/* download the playback stream of asset idx to VIDEO_TMP_PATH. + * returns 0 on success, 1 if the user cancelled, -1 on error. */ +static int download_video(int idx, char *err, size_t errlen) +{ + char url[700]; + snprintf(url, sizeof(url), "%s/api/assets/%s/video/playback", + g_server, g_asset_ids[idx]); + g_dl_what = "video"; + return download_to_file(url, VIDEO_TMP_PATH, err, errlen); +} + static void *av_alloc(void *p, uint32_t alignment, uint32_t size) { void *r = memalign(alignment, size); diff --git a/src/viewrot.c b/src/viewrot.c new file mode 100644 index 0000000..5832ca3 --- /dev/null +++ b/src/viewrot.c @@ -0,0 +1,176 @@ +/* vitaImmich unity build — part of one translation unit. + * This file is #included by src/main.c (in order) and shares all + * file-scope state with the other parts; it is NOT compiled on its own. + * Only src/main.c is listed in CMakeLists.txt. Editors/clangd may flag + * undefined symbols here because of that — those are expected. + */ + +/* ------------------------------------------------------------------ */ +/* turned photo viewer */ +/* */ +/* L / R in the full-screen viewer turn the whole viewer (not the */ +/* photo) 90 degrees, so the Vita can be held upright and portrait */ +/* photos fill the screen. While turned, the viewer lays itself out in */ +/* its logical screen (SCREEN_W x SCREEN_H = 544x960 when upright), */ +/* draws into an off-screen render target of that size, and the target */ +/* is drawn rotated onto the real 960x544 screen. Buttons, sticks and */ +/* touch are mapped into the logical screen so "up" stays up for the */ +/* person holding the Vita. */ +/* ------------------------------------------------------------------ */ + +/* vita2d keeps its projection in this global and uploads it with every + * draw; it is set once for 960x544, so render targets of another size need + * it swapped while they're drawn into */ +extern float _vita2d_ortho_matrix[4 * 4]; +void matrix_init_orthographic(float *m, float left, float right, float bottom, + float top, float near, float far); + +static int g_view_rot; /* 0 / 90 / 180 / 270, degrees clockwise */ +static float g_view_rot_anim; /* degrees still to turn (eases to 0) */ +static vita2d_texture *g_view_rt[2]; /* [0] 960x544, [1] 544x960 */ +static int g_view_rt_on; /* this frame draws into a render target */ + +static void view_ortho(int w, int h) +{ + matrix_init_orthographic(_vita2d_ortho_matrix, 0.0f, w, h, 0.0f, + 0.0f, 1.0f); +} + +/* turn the viewer by +-90 degrees (R: clockwise, L: counter-clockwise) */ +static void view_turn(int deg) +{ + g_view_rot = (g_view_rot + deg + 360) % 360; + g_view_rot_anim -= deg; /* start from the old angle, ease in */ + log_line("viewer turned to %d", g_view_rot); +} + +/* the viewer's logical screen for this frame */ +static void view_enter(void) +{ + int upright = (g_view_rot % 180) != 0; + g_screen_w = upright ? 544 : 960; + g_screen_h = upright ? 960 : 544; +} + +/* back to the real screen (before anything that draws on its own, like + * the video player or a download progress screen) */ +static void view_leave(void) +{ + g_screen_w = 960; + g_screen_h = 544; +} + +/* a screen-space direction as seen in the turned viewer */ +static void view_vec(float sx, float sy, float *lx, float *ly) +{ + switch (g_view_rot) { + case 90: *lx = sy; *ly = -sx; break; + case 180: *lx = -sx; *ly = -sy; break; + case 270: *lx = -sy; *ly = sx; break; + default: *lx = sx; *ly = sy; break; + } +} + +/* d-pad bits as seen in the turned viewer */ +static unsigned int view_buttons(unsigned int b) +{ + static const struct { unsigned int bit; int x, y; } d[4] = { + { SCE_CTRL_UP, 0, -1 }, { SCE_CTRL_DOWN, 0, 1 }, + { SCE_CTRL_LEFT, -1, 0 }, { SCE_CTRL_RIGHT, 1, 0 }, + }; + unsigned int out = b & ~(SCE_CTRL_UP | SCE_CTRL_DOWN | + SCE_CTRL_LEFT | SCE_CTRL_RIGHT); + for (int i = 0; i < 4; i++) { + if (!(b & d[i].bit)) + continue; + float lx, ly; + view_vec(d[i].x, d[i].y, &lx, &ly); + for (int j = 0; j < 4; j++) + if (d[j].x == (int)lx && d[j].y == (int)ly) + out |= d[j].bit; + } + return out; +} + +/* a stick (0..255, centre 128) as seen in the turned viewer */ +static void view_stick(unsigned char *x, unsigned char *y) +{ + float lx, ly; + view_vec(*x - 128.0f, *y - 128.0f, &lx, &ly); + lx += 128.0f; + ly += 128.0f; + *x = lx < 0 ? 0 : lx > 255 ? 255 : (unsigned char)lx; + *y = ly < 0 ? 0 : ly > 255 ? 255 : (unsigned char)ly; +} + +/* a touch point on the real screen -> the turned viewer's screen */ +static void view_touch(float *x, float *y) +{ + float sx = *x, sy = *y; + switch (g_view_rot) { + case 90: *x = sy; *y = 960 - sx; break; + case 180: *x = 960 - sx; *y = 544 - sy; break; + case 270: *x = 544 - sy; *y = sx; break; + default: break; + } +} + +/* start the viewer's frame: straight to the screen when not turned, + * else into the render target of the logical screen's size */ +static void view_begin_drawing(void) +{ + g_view_rt_on = 0; + if (g_view_rot != 0 || g_view_rot_anim != 0.0f) { + int k = (g_view_rot % 180) != 0; + if (!g_view_rt[k]) + g_view_rt[k] = vita2d_create_empty_texture_rendertarget( + SCREEN_W, SCREEN_H, SCE_GXM_TEXTURE_FORMAT_A8B8G8R8); + g_view_rt_on = g_view_rt[k] != NULL; + if (g_view_rt_on) { + /* vita2d_start_drawing() is what empties the per-frame + * vertex pool; the _advanced variant doesn't, so without + * this every turned frame leaks pool space until a draw + * gets NULL and crashes */ + vita2d_pool_reset(); + vita2d_start_drawing_advanced(g_view_rt[k], + SCE_GXM_SCENE_FRAGMENT_SET_DEPENDENCY); + vita2d_clear_screen(); + view_ortho(SCREEN_W, SCREEN_H); + return; + } + } + vita2d_start_drawing(); + vita2d_clear_screen(); +} + +/* finish the viewer's frame: draw the render target turned (and scaled + * to fit while the turn animates) onto the screen, then swap */ +static void view_end_drawing(void) +{ + vita2d_end_drawing(); + if (g_view_rt_on) { + vita2d_texture *rt = g_view_rt[(g_view_rot % 180) != 0]; + float lw = SCREEN_W, lh = SCREEN_H; + view_leave(); + view_ortho(SCREEN_W, SCREEN_H); + vita2d_start_drawing_advanced(NULL, + SCE_GXM_SCENE_VERTEX_WAIT_FOR_DEPENDENCY); + vita2d_clear_screen(); + float rad = (g_view_rot + g_view_rot_anim) * 3.14159265f / 180.0f; + float c = fabsf(cosf(rad)), s = fabsf(sinf(rad)); + float bw = lw * c + lh * s, bh = lw * s + lh * c; + float k = fminf(1.0f, fminf(SCREEN_W / bw, SCREEN_H / bh)); + vita2d_draw_texture_scale_rotate(rt, SCREEN_W / 2.0f, + SCREEN_H / 2.0f, k, k, rad); + vita2d_end_drawing(); + } +#ifdef VIMM_TEST_INPUT + static unsigned int n; + if (++n % 120 == 0) + log_line("viewer pool free: %u", vita2d_pool_free_space()); +#endif + vita2d_swap_buffers(); + g_view_rot_anim *= 0.7f; + if (fabsf(g_view_rot_anim) < 0.5f) + g_view_rot_anim = 0.0f; +}