From e54179f5c3a02762e76bdf835c5eaa33d4d0d81d Mon Sep 17 00:00:00 2001 From: Sads Date: Sat, 13 Jun 2026 17:39:01 +0200 Subject: [PATCH] Startup logo spinner, search-bar focus nav, resume rescan, logo + AA play Startup: bundle the Immich logo (app0:logo.png, transparent + AA) and show a spinning-logo loading screen while the first page loads on a thread. Search bar: logo to the left of the field; d-pad up from the top row focuses the bar, left/right move across field/map/cloud (indigo ring), X activates, down or [] returns to the grid. The selection square stays hidden while focused. Cloud: "Made by SadsArches with love" credit; the O/Back button lives on the See-details menu (top-right, tappable). Reverted the back circle from the image/detail view. Resume from sleep: an RTC wall-clock jump triggers an append-only camera re-scan (startup reserves array headroom so it can't realloc under the worker /sync threads). Play overlay redrawn with the AA disc + a baked play-triangle glyph. Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 1 + assets/immich_logo.png | Bin 0 -> 8744 bytes goals.md | 15 +++ src/main.c | 285 ++++++++++++++++++++++++++++++++++------- 4 files changed, 256 insertions(+), 45 deletions(-) create mode 100644 assets/immich_logo.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a0d660..89e7ccd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,4 +56,5 @@ vita_create_vpk(${PROJECT_NAME}.vpk ${VITA_TITLEID} ${PROJECT_NAME}.self FILE ${CMAKE_SOURCE_DIR}/assets/sce_sys/icon0.png sce_sys/icon0.png FILE ${CMAKE_SOURCE_DIR}/assets/Overpass-SemiBold.ttf font.ttf FILE ${CMAKE_SOURCE_DIR}/assets/OFL-Overpass.txt OFL-Overpass.txt + FILE ${CMAKE_SOURCE_DIR}/assets/immich_logo.png logo.png ) diff --git a/assets/immich_logo.png b/assets/immich_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f44930f284c8cd18d8ede8cc4a08b5f237bd2f3c GIT binary patch literal 8744 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 SAzdQ?00003 s) and append-only re-scan the + camera folders (headroom reserved at startup so no realloc races the + worker/sync threads). +- [x] Pressing up on the d-pad (from the top row) focuses the search bar + (indigo ring); down returns to the grid, X opens the keyboard. +- [x] Immich logo (bundled app0:logo.png, white bg made transparent) to the + left of the search bar. +- [x] "Made by SadsArches with love" credit on the cloud menu. +- [x] Spinning Immich logo on the startup loading screen (first page fetched + on a thread so the logo actually spins), loading text below. +- [x] Anti-alias the video play overlay (AA disc + baked play-triangle glyph). +- [x] O (back) circle button top-right in the detail view (tappable + O). + ## 0. UI polish (current round) - [x] Hide the white selection square while touch-scrolling; show it again when the selection moves with the d-pad. diff --git a/src/main.c b/src/main.c index 8130b11..ebedc4d 100644 --- a/src/main.c +++ b/src/main.c @@ -104,6 +104,7 @@ int BZ2_bzDecompressEnd(void *strm) { return -1; } static vita2d_pgf *g_font; /* system font, fallback */ static vita2d_font *g_ttf; /* bundled Overpass (Immich's face) */ +static vita2d_texture *g_logo; /* bundled Immich logo (app0:logo.png) */ /* server-asset arrays. dynamically grown (doubling) as pages are fetched, so * the library is bounded only by memory, not a fixed cap. grown on the main @@ -391,6 +392,26 @@ static void draw_centered(int y, uint32_t color, const char *text) draw_text((SCREEN_W - w) / 2, y, color, 1.0f, text); } +/* startup loading screen: the Immich logo spinning, with text below it */ +static void draw_loading(const char *text, unsigned int frame) +{ + vita2d_start_drawing(); + vita2d_clear_screen(); + float cx = SCREEN_W / 2.0f, cy = SCREEN_H / 2.0f - 30.0f; + if (g_logo) { + float lw = vita2d_texture_get_width(g_logo); + float lh = vita2d_texture_get_height(g_logo); + float scale = 110.0f / lw; + vita2d_draw_texture_scale_rotate_hotspot(g_logo, cx, cy, + scale, scale, frame * 0.10f, lw / 2.0f, lh / 2.0f); + } + int w = text_width(1.0f, text); + draw_text((SCREEN_W - w) / 2, cy + 90.0f, + RGBA8(225, 225, 230, 255), 1.0f, text); + vita2d_end_drawing(); + vita2d_swap_buffers(); +} + static void show_status(const char *fmt, ...) { char buf[512]; @@ -1957,7 +1978,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 = 6 }; /* a small fixed set of shapes (pill, buttons, ...) */ + enum { NSLOT = 8 }; /* a small fixed set of shapes (pill, buttons, ...) */ static vita2d_texture *cache[NSLOT]; static int cw[NSLOT], ch[NSLOT]; static float cr[NSLOT]; @@ -2141,12 +2162,19 @@ static int cov_circle(float px, float py, int s) return d <= R && d >= R - W; } +/* a filled right-pointing play triangle */ +static int cov_play(float px, float py, int s) +{ + return in_tri(px, py, s * 0.36f, s * 0.27f, + s * 0.36f, s * 0.73f, s * 0.76f, s * 0.5f); +} + /* cache one texture per glyph; all are drawn at a fixed size each frame */ static vita2d_texture *icon_tex(int which, int s) { - static vita2d_texture *cache[7]; - static int cs[7]; - if (which < 0 || which > 6) + static vita2d_texture *cache[8]; + static int cs[8]; + if (which < 0 || which > 7) return NULL; if (cache[which] && cs[which] == s) return cache[which]; @@ -2160,7 +2188,8 @@ static vita2d_texture *icon_tex(int which, int s) which == 2 ? cov_cloud : which == 3 ? cov_tri : which == 4 ? cov_sq : - which == 5 ? cov_cross : cov_circle; + which == 5 ? cov_cross : + which == 6 ? cov_circle : cov_play; cache[which] = bake_icon(s, fn); cs[which] = s; return cache[which]; @@ -2172,6 +2201,7 @@ static vita2d_texture *icon_tex(int which, int s) #define ICON_SQUARE 4 #define ICON_CROSS 5 #define ICON_CIRCLE 6 +#define ICON_PLAY 7 /* a DualShock-style face button: a dark disc (subtle lighter rim) centred at * (cx,cy) with the coloured glyph on top. each glyph keeps a per-call size. */ @@ -2244,8 +2274,9 @@ static void draw_ps_button(float cx, float cy, float d, int icon, #define BTN2_CX ((float)SCREEN_W - BAR_M - BTN_SZ / 2) /* cloud (rightmost) */ #define BTN1_CX (BTN2_CX - BTN_SZ - BTN_GAP) /* map */ #define BTN_CY (BAR_Y + BAR_H / 2) -#define BAR_W (BTN1_CX - BTN_SZ / 2 - BTN_GAP - BAR_M) /* pill width */ -#define BAR_CLEAR_CX (BAR_M + BAR_W - 22.0f) /* centre of the clear chip */ +#define BAR_PX (BAR_M + 42.0f) /* pill left x (logo to its left) */ +#define BAR_W (BTN1_CX - BTN_SZ / 2 - BTN_GAP - BAR_PX) /* pill width */ +#define BAR_CLEAR_CX (BAR_PX + BAR_W - 22.0f) /* centre of the clear chip */ /* one future-feature button: rounded-square background + a centred AA glyph */ static void draw_bar_button(float cx, float cy, int icon) @@ -2261,32 +2292,55 @@ static void draw_bar_button(float cx, float cy, int icon) RGBA8(190, 190, 196, 255)); } -/* draw the bar shifted vertically by `yoff` (Square slides it out of view) */ -static void draw_search_bar(float yoff) +/* draw the bar shifted vertically by `yoff` (Square slides it out of view); + * `focus` highlights the focused element (1=field, 2=map, 3=cloud) */ +static void draw_search_bar(float yoff, int focus) { unsigned int field = RGBA8(38, 38, 42, 255); + unsigned int hi = RGBA8(94, 110, 215, 255); /* focus ring (indigo) */ float by = BAR_Y + yoff, cy = BTN_CY + yoff; /* opaque strip so grid items scroll cleanly underneath the bar */ vita2d_draw_rectangle(0, yoff, SCREEN_W, SEARCH_H, RGBA8(16, 16, 16, 255)); + /* Immich logo to the left of the search field */ + if (g_logo) { + float ls = (BAR_H - 6.0f) / vita2d_texture_get_width(g_logo); + vita2d_draw_texture_scale(g_logo, BAR_M, by + 3.0f, ls, ls); + } + /* focus ring behind the field */ + if (focus == 1) { + vita2d_texture *ring = rounded_mask_tex((int)BAR_W + 6, + (int)BAR_H + 6, BAR_R + 3); + if (ring) + vita2d_draw_texture_tint(ring, BAR_PX - 3, by - 3, hi); + } /* borderless field: an AA rounded-rect mask tinted to the field colour */ vita2d_texture *mask = rounded_mask_tex((int)BAR_W, (int)BAR_H, BAR_R); if (mask) - vita2d_draw_texture_tint(mask, BAR_M, by, field); + vita2d_draw_texture_tint(mask, BAR_PX, by, field); else - vita2d_draw_rectangle(BAR_M, by, BAR_W, BAR_H, field); + vita2d_draw_rectangle(BAR_PX, by, BAR_W, BAR_H, field); int isz = 24; vita2d_texture *icon = icon_tex(ICON_SEARCH, isz); if (icon) - vita2d_draw_texture_tint(icon, BAR_M + 22 - isz / 2.0f, + vita2d_draw_texture_tint(icon, BAR_PX + 22 - isz / 2.0f, cy - isz / 2.0f, RGBA8(190, 190, 196, 255)); + /* focus rings behind the placeholder buttons */ + if (focus == 2 || focus == 3) { + vita2d_texture *r = rounded_mask_tex((int)BTN_SZ + 6, + (int)BTN_SZ + 6, 14.0f); + float fcx = (focus == 2) ? BTN1_CX : BTN2_CX; + if (r) + vita2d_draw_texture_tint(r, fcx - (BTN_SZ + 6) / 2, + cy - (BTN_SZ + 6) / 2, hi); + } /* placeholder buttons (map, cloud) for future features */ draw_bar_button(BTN1_CX, cy, ICON_MAP); draw_bar_button(BTN2_CX, cy, ICON_CLOUD); - float tx = BAR_M + 44, ty = by + BAR_H - 13; + float tx = BAR_PX + 44, ty = by + BAR_H - 13; if (g_search_active && g_search_query[0]) { char q[64]; snprintf(q, sizeof(q), "%.40s", g_search_query); @@ -2521,20 +2575,16 @@ static void draw_throbber(float cx, float cy, float r, unsigned int frame) } } -/* translucent play button over a video's poster in the detail view */ +/* translucent play button over a video's poster in the detail view (AA) */ static void draw_play_overlay(void) { float cx = SCREEN_W / 2.0f, cy = SCREEN_H / 2.0f; - vita2d_draw_fill_circle(cx, cy, 46.0f, RGBA8(20, 20, 20, 150)); - vita2d_color_vertex *v = - vita2d_pool_memalign(3 * sizeof(*v), sizeof(*v)); - if (!v) - return; - unsigned int col = RGBA8(255, 255, 255, 220); - v[0] = (vita2d_color_vertex){ cx - 14.0f, cy - 24.0f, 0.5f, col }; - v[1] = (vita2d_color_vertex){ cx - 14.0f, cy + 24.0f, 0.5f, col }; - v[2] = (vita2d_color_vertex){ cx + 28.0f, cy, 0.5f, col }; - vita2d_draw_array(SCE_GXM_PRIMITIVE_TRIANGLES, v, 3); + draw_disc(cx, cy, 92.0f, RGBA8(20, 20, 20, 150)); + int gs = 56; + vita2d_texture *p = icon_tex(ICON_PLAY, gs); + if (p) + vita2d_draw_texture_tint(p, cx - gs / 2.0f, cy - gs / 2.0f, + RGBA8(255, 255, 255, 235)); } static void draw_error_detail(int idx) @@ -3281,6 +3331,11 @@ static int is_media_name(const char *name, int *is_video) return 0; } +/* set while re-scanning after a resume from sleep: scan_dir then only appends + * genuinely-new files into the headroom (no realloc; the worker/sync threads + * are reading the arrays) */ +static int g_rescan; + /* recurse a directory (up to depth levels) collecting camera media */ static void scan_dir(const char *path, int depth) { @@ -3328,12 +3383,26 @@ static void scan_dir(const char *path, int depth) continue; } - if (!grow_locals(g_local_count + 1)) { + /* rescan (resume from sleep): skip files we already know, and + * never realloc while the sync/worker threads are reading the + * arrays — only fill into the headroom reserved at startup */ + if (g_rescan) { + int known = 0; + for (int k = 0; k < g_local_count && !known; k++) + known = !strcmp(g_local_path[k], full); + if (known) + continue; + if (g_local_count >= g_local_cap) { + log_line("rescan: no headroom at %d files", + g_local_count); + break; + } + } else if (!grow_locals(g_local_count + 1)) { log_line("scan: out of memory at %d local files", g_local_count); break; } - int j = g_local_count++; + int j = g_local_count; /* fill, then publish (count++) */ memcpy(g_local_path[j], full, pl + 1 + nl + 1); g_local_is_video[j] = is_video; g_local_size[j] = (long long)ent.d_stat.st_size; @@ -3345,6 +3414,8 @@ static void scan_dir(const char *path, int depth) g_local_server_id[j][0] = '\0'; g_local_err[j][0] = '\0'; g_local_state[j] = SYNC_UNSCANNED; + __sync_synchronize(); + g_local_count = j + 1; } sceIoDclose(dfd); log_line("scan: %s: %d media so far", path, g_local_count); @@ -3374,6 +3445,27 @@ static void scan_local_media(void) log_line("local scan: %d files", g_local_count); } +/* append-only re-scan of the camera folders (resume from sleep). same dirs as + * scan_local_media, but g_rescan keeps it from touching known entries or + * reallocating. returns how many new files were appended. */ +static int rescan_local_new(void) +{ + int before = g_local_count; + g_rescan = 1; + if (g_syncdir_count > 0) { + for (int i = 0; i < g_syncdir_count; i++) + scan_dir(g_syncdirs[i], 3); + } else { + scan_dir(g_photo0_mounted ? "photo0:" : "ux0:picture", 3); + scan_dir("ux0:video/CAMERA", 3); + } + g_rescan = 0; + int added = g_local_count - before; + if (added > 0) + log_line("rescan: %d new local file(s) after resume", added); + return added; +} + static int sha1_file_hex(const char *path, char out[41]) { FILE *f = fopen(path, "rb"); @@ -3830,12 +3922,25 @@ static int count_state(int st) enum { MODE_GRID, MODE_DETAIL, MODE_CLOUD, MODE_CLOUD_DETAILS }; +/* the first library page is fetched on this thread so the main thread can + * spin the loading logo while it waits */ +static volatile int g_init_load_done; +static int init_load_thread(SceSize args, void *argp) +{ + (void)args; (void)argp; + fetch_page(0); + __sync_synchronize(); + g_init_load_done = 1; + return 0; +} + int main(void) { vita2d_init(); vita2d_set_clear_color(RGBA8(16, 16, 16, 255)); g_font = vita2d_load_default_pgf(); g_ttf = vita2d_load_font_file("app0:font.ttf"); + g_logo = vita2d_load_PNG_file("app0:logo.png"); /* on-screen keyboard (smart search). the IME runs as a common dialog; * the config tells it the system language + enter/cancel button map */ @@ -3915,10 +4020,22 @@ int main(void) log_line("server='%s' len=%d hex=%s", g_server, n, hex); } - show_status("Loading library from %s ...", g_server); - fetch_page(1); + /* spinning-logo loading screen while the first page loads on a thread */ + g_init_load_done = 0; + SceUID lt = sceKernelCreateThread("init_load", init_load_thread, + 0x10000100, 256 * 1024, 0, 0, NULL); + if (lt >= 0) { + sceKernelStartThread(lt, 0, NULL); + unsigned int lf = 0; + while (!g_init_load_done) { + draw_loading("Loading your library...", lf++); + sceDisplayWaitVblankStart(); + } + } else { + fetch_page(1); /* fallback: blocking, no spinner */ + } if (g_asset_count == 0) - fatal_error(NULL, "Server returned no assets"); + fatal_error(NULL, "Server returned no assets (see log.txt)"); SceUID worker = sceKernelCreateThread("thumb_loader", worker_thread, 0x10000100, 256 * 1024, 0, 0, NULL); @@ -3927,8 +4044,11 @@ int main(void) /* scan the Vita's own camera media (fast: paths + stat only), build * the merged timeline, then let the sync thread hash + dup-check it */ - show_status("Scanning local media..."); + draw_loading("Scanning local media...", 0); scan_local_media(); + /* reserve headroom so a resume-from-sleep rescan can append new camera + * files without reallocating while the worker/sync threads read */ + grow_locals(g_local_count + 512); rebuild_display(); SceUID syncw = sceKernelCreateThread("sync_worker", sync_thread, @@ -3944,6 +4064,8 @@ int main(void) int show_sel = 1; /* selection square: shown for d-pad, hidden for touch */ int bar_shown = 1; /* top search bar visible */ float bar_hidden = 0.0f;/* animated pixels the bar is slid up (0..SEARCH_H) */ + int bar_focus = 0; /* search bar focused (d-pad up from the top row) */ + uint64_t last_tick = 0; /* wall-clock (incl. sleep) to detect resume */ /* periodic check for photos added to the server while we run */ uint64_t last_poll = sceKernelGetProcessTimeWide(); int grid_last_sel = -1; /* sel at the previous frame */ @@ -3992,6 +4114,22 @@ int main(void) frame++; + /* resume from sleep: wall-clock (RTC) keeps running while the + * Vita is suspended, so a big jump between frames means we just + * woke — re-scan the camera folders for photos taken meanwhile */ + { + SceRtcTick rt; + if (sceRtcGetCurrentTick(&rt) == 0) { + if (last_tick && + rt.tick - last_tick > 3000000ULL) { + if (rescan_local_new() > 0) + rebuild_keep_view(&sel, &scroll, + &target); + } + last_tick = rt.tick; + } + } + /* auto-backup: while enabled, queue any newly-found local-only * photos for upload (~1/s; the sync thread drains the queue). * this covers both launch and photos found mid-session. */ @@ -4042,18 +4180,42 @@ int main(void) (pressed & SCE_CTRL_CIRCLE) != 0; int do_open_cloud = 0; /* tap on the cloud button */ - /* SQUARE slides the top bar in/out; animate the offset */ - if (pressed & SCE_CTRL_SQUARE) + /* SQUARE slides the top bar in/out (when not focused); + * while focused it drops focus back to the grid */ + if ((pressed & SCE_CTRL_SQUARE) && !bar_focus) bar_shown = !bar_shown; float bar_t = bar_shown ? 0.0f : (float)SEARCH_H; bar_hidden += (bar_t - bar_hidden) * 0.3f; if (fabsf(bar_t - bar_hidden) < 0.5f) bar_hidden = bar_t; - /* the d-pad (and month jumps) bring the selection square - * back; a touch drag hides it again (set below) */ - if (nav & dirs) + /* the d-pad brings the selection square back — but not + * while the bar is focused; a touch drag hides it again */ + if ((nav & dirs) && !bar_focus) show_sel = 1; + if (bar_focus) { + /* bar focused (1=field, 2=map, 3=cloud): left/right + * move across the elements, down or [] returns to + * the grid, X activates the focused element */ + show_sel = 0; + if ((pressed & SCE_CTRL_SQUARE) || + (nav & SCE_CTRL_DOWN)) { + bar_focus = 0; + show_sel = 1; + } else { + if ((nav & SCE_CTRL_LEFT) && bar_focus > 1) + bar_focus--; + if ((nav & SCE_CTRL_RIGHT) && bar_focus < 3) + bar_focus++; + if (pressed & SCE_CTRL_CROSS) { + if (bar_focus == 1) + do_search_open = 1; + else if (bar_focus == 3) + do_open_cloud = 1; + /* map (2): placeholder, no-op */ + } + } + } else { if (nav & SCE_CTRL_RIGHT) sel++; if (nav & SCE_CTRL_LEFT) @@ -4064,8 +4226,16 @@ int main(void) sel = g_disp_count - 1; if (nav & SCE_CTRL_DOWN) sel = nav_row(sel, +1); - if (nav & SCE_CTRL_UP) - sel = nav_row(sel, -1); + if (nav & SCE_CTRL_UP) { + int nr = nav_row(sel, -1); + if (nr == sel) { /* already on the top row */ + bar_focus = 1; + bar_shown = 1; /* reveal the bar if hidden */ + show_sel = 0; + } else { + sel = nr; + } + } if ((pressed & (SCE_CTRL_RTRIGGER | SCE_CTRL_LTRIGGER)) && g_disp_count > 0) { @@ -4092,6 +4262,7 @@ int main(void) if (sel >= 0 && sel < g_disp_count) target = g_item_y[sel] - HEADER_H - SEARCH_H; } + } /* end !bar_focus */ /* Track how long the selection has held still. A held * d-pad auto-repeats one row every few frames; while it's @@ -4112,7 +4283,8 @@ int main(void) } int scrolling_fast = (grid_settle < 12); - if ((pressed & SCE_CTRL_CROSS) && g_disp_count > 0) { + if ((pressed & SCE_CTRL_CROSS) && g_disp_count > 0 && + !bar_focus) { /* photos and videos both open the detail * view; a video shows its poster with the * play button there */ @@ -4167,6 +4339,7 @@ int main(void) touch_start_y = ty; touch_start_scroll = scroll; touch_vel = 0; + bar_focus = 0; /* touching cancels bar focus */ /* a contact on the (visible) bar opens * the keyboard instead of scrolling */ touch_on_bar = @@ -4209,7 +4382,7 @@ int main(void) /* cloud button: open the * server/backup page */ do_open_cloud = 1; - } else if (touch_x > BAR_M + BAR_W) { + } else if (touch_x > BAR_PX + BAR_W) { /* map button (placeholder) */ } else if (g_search_active && touch_x > BAR_CLEAR_CX - 16) @@ -4460,7 +4633,7 @@ int main(void) /* pinned Immich-style search bar, slid up by bar_hidden * (Square toggles it); covers items up to its lower edge */ - draw_search_bar(-bar_hidden); + draw_search_bar(-bar_hidden, bar_focus); char hud[200]; if (g_search_active) @@ -4910,10 +5083,8 @@ pf_skip: g_disp_count > 0 ? disp_date(sel) : "", xhint); draw_hud(hud); - /* ...and a discreet corner throbber while the - * full-res image is still on its way — but only when - * a preview is showing; with no preview at all the - * big centered throbber is already up */ + /* discreet corner throbber while the full-res image loads, + * only when a preview is showing */ if (detail_loading && disp_thumb(sel)) draw_throbber(SCREEN_W - 36.0f, 36.0f, 14.0f, frame); @@ -5119,12 +5290,32 @@ pf_skip: draw_ps_button(ubx - 23, bty + bth / 2, 38, ICON_TRI, RGBA8(95, 205, 130, 255), 28); + draw_centered(SCREEN_H - 56, RGBA8(150, 150, 158, 255), + "Made by SadsArches with love"); draw_hud("X / tap details O back"); vita2d_end_drawing(); vita2d_swap_buffers(); } else if (mode == MODE_CLOUD_DETAILS) { /* scrollable list of not-backed-up local files + status */ - if (pressed & SCE_CTRL_CIRCLE) { + float bkd = 36, bkcx = SCREEN_W - 30, bkcy = 36; + int do_back = (pressed & SCE_CTRL_CIRCLE) != 0; + /* touch: tap the top-right Back button */ + { + SceTouchData td; + sceTouchPeek(SCE_TOUCH_PORT_FRONT, &td, 1); + if (td.reportNum > 0) { + float tx = td.report[0].x * 0.5f; + float ty = td.report[0].y * 0.5f; + if (!touch_active) { + touch_active = 1; + if (ty < bkcy + bkd && tx > bkcx - bkd) + do_back = 1; + } + } else { + touch_active = 0; + } + } + if (do_back) { mode = MODE_CLOUD; continue; } @@ -5185,6 +5376,10 @@ pf_skip: RGBA8(120, 200, 120, 255), "Everything is backed up"); + /* top-right Back button (also press O) */ + draw_ps_button(bkcx, bkcy, bkd, ICON_CIRCLE, + RGBA8(235, 90, 85, 255), 26); + draw_hud("Up/Down scroll O back"); vita2d_end_drawing(); vita2d_swap_buffers();