Cloud/backup page, map+cloud buttons, drop SELECT sync menu

Search bar: add two right-side buttons (map placeholder, cloud) drawn as
anti-aliased glyph masks; rework the magnifier into a thin ring + handle.

Cloud button opens a Server & Backup page: server storage used/total with a
usage bar, server version and URL, app version, and on-device image counts
(detected / backed up / remaining), fetched lazily via /api/server/storage
and /api/server/version (with server-info legacy fallback). A "See details"
button opens a scrollable list of not-backed-up files with per-item status
(waiting / hashing / uploading / failed). An "Upload all" button (and the
detail counts) replace the removed sync screen.

Remove the SELECT sync overview (MODE_SYNC) and its now-unused helpers; the
upload-all action it hosted now lives on the cloud page so backups stay
reachable.
This commit is contained in:
Sads
2026-06-13 15:18:34 +02:00
parent ba7a30c41d
commit 4aef8042c4
+452 -228
View File
@@ -65,6 +65,7 @@
#define PAGE_SIZE 100
#define DATELEN 20 /* "YYYY-MM-DDTHH:MM:SS" + NUL, sortable */
#define APP_VERSION "1.0" /* keep in step with CMakeLists VITA_VERSION */
#define CONFIG_DIR "ux0:data/vitaimmich"
#define CONFIG_PATH CONFIG_DIR "/config.txt"
@@ -189,6 +190,14 @@ static int *g_search_idx;
static int g_search_count;
static int g_search_cap;
/* server info shown on the cloud/backup page; fetched lazily the first time
* the page is opened. g_srv_state: 0 not fetched, 1 ok, -1 failed. */
static int g_srv_state;
static char g_srv_use[40]; /* disk used, human ("1.4 TiB") */
static char g_srv_total[40]; /* disk size, human ("1.8 TiB") */
static int g_srv_pct; /* disk usage percentage */
static char g_srv_version[40]; /* "v1.119.0" */
/* Texture recycling pool. Freeing a texture unmaps its memblock, and the
* GPU side (notably Vita3K's texture cache, which re-reads guest memory of
* cached textures at its own pace) may still touch it afterwards — freeing
@@ -724,6 +733,95 @@ static CURLcode http_request(const char *url, const char *body,
return res;
}
/* copy the value of a top-level JSON key into `out` (raw token text, works
* for both string and number values). returns 1 if found. small flat
* responses only (server storage / version). */
static int json_get(const char *js, size_t len, const char *key,
char *out, size_t outlen)
{
jsmn_parser p;
jsmn_init(&p);
int n = jsmn_parse(&p, js, len, NULL, 0);
if (n <= 0)
return 0;
jsmntok_t *t = malloc(sizeof(*t) * n);
if (!t)
return 0;
jsmn_init(&p);
n = jsmn_parse(&p, js, len, t, n);
int kl = (int)strlen(key), got = 0;
for (int i = 1; i + 1 < n; i++) {
if (t[i].type == JSMN_STRING && t[i].parent == 0 &&
t[i].end - t[i].start == kl &&
!strncmp(js + t[i].start, key, kl)) {
int vl = t[i + 1].end - t[i + 1].start;
if (vl >= (int)outlen)
vl = (int)outlen - 1;
memcpy(out, js + t[i + 1].start, vl);
out[vl] = '\0';
got = 1;
break;
}
}
free(t);
return got;
}
/* GET a JSON endpoint, trying the modern path then a legacy fallback. fills
* `out`/`code`; caller frees out->data. returns CURLcode of the call used. */
static CURLcode server_get(const char *path, const char *legacy,
membuf *out, long *code)
{
char url[600];
snprintf(url, sizeof(url), "%s%s", g_server, path);
CURLcode r = http_request(url, NULL, out, code, NULL, 0);
if (r == CURLE_OK && (*code < 200 || *code >= 300) && legacy) {
free(out->data);
snprintf(url, sizeof(url), "%s%s", g_server, legacy);
r = http_request(url, NULL, out, code, NULL, 0);
}
return r;
}
/* fetch disk usage + server version into the g_srv_* fields (main thread) */
static void fetch_server_info(void)
{
membuf buf;
long code;
CURLcode r = server_get("/api/server/storage",
"/api/server-info/storage", &buf, &code);
if (r == CURLE_OK && code >= 200 && code < 300 && buf.data) {
char pct[24] = "";
if (!json_get(buf.data, buf.size, "diskUse",
g_srv_use, sizeof(g_srv_use)))
g_srv_use[0] = '\0';
if (!json_get(buf.data, buf.size, "diskSize",
g_srv_total, sizeof(g_srv_total)))
g_srv_total[0] = '\0';
if (json_get(buf.data, buf.size, "diskUsagePercentage",
pct, sizeof(pct)))
g_srv_pct = (int)(atof(pct) + 0.5);
g_srv_state = 1;
} else {
log_line("server storage: curl %d http %ld", r, code);
g_srv_state = -1;
}
free(buf.data);
r = server_get("/api/server/version", "/api/server-info/version",
&buf, &code);
if (r == CURLE_OK && code >= 200 && code < 300 && buf.data) {
char mj[12] = "", mn[12] = "", pt[12] = "";
json_get(buf.data, buf.size, "major", mj, sizeof(mj));
json_get(buf.data, buf.size, "minor", mn, sizeof(mn));
json_get(buf.data, buf.size, "patch", pt, sizeof(pt));
snprintf(g_srv_version, sizeof(g_srv_version), "v%s.%s.%s",
mj[0] ? mj : "?", mn[0] ? mn : "?", pt[0] ? pt : "?");
}
free(buf.data);
}
/* ------------------------------------------------------------------ */
/* JPEG decoding */
/* */
@@ -1854,16 +1952,16 @@ 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)
{
static vita2d_texture *cache;
static int cw, ch;
static float cr;
if (cache && cw == w && ch == h && cr == r)
return cache;
if (cache) {
vita2d_wait_rendering_done();
vita2d_free_texture(cache);
cache = NULL;
}
enum { NSLOT = 6 }; /* a small fixed set of shapes (pill, buttons, ...) */
static vita2d_texture *cache[NSLOT];
static int cw[NSLOT], ch[NSLOT];
static float cr[NSLOT];
static int n;
for (int i = 0; i < n; i++)
if (cw[i] == w && ch[i] == h && cr[i] == r)
return cache[i];
if (n >= NSLOT)
return NULL;
vita2d_texture *t = vita2d_create_empty_texture_format(w, h,
SCE_GXM_TEXTURE_FORMAT_A8B8G8R8);
if (!t)
@@ -1892,27 +1990,85 @@ static vita2d_texture *rounded_mask_tex(int w, int h, float r)
hits * 255 / 16);
}
}
cache = t;
cw = w;
ch = h;
cr = r;
return cache;
cache[n] = t;
cw[n] = w;
ch[n] = h;
cr[n] = r;
n++;
return t;
}
/* a line-art magnifying glass baked into an AA white mask (4x4 supersampled):
* a thin ring outline plus a straight, round-capped handle off the lower
* right. drawn tinted to the icon colour. cached at one size. */
static vita2d_texture *search_icon_tex(int s)
/* ---- AA glyph icons (magnifier / map pin / cloud) -------------------- */
/* each is a coverage predicate over icon-local pixel coords (0..s); bake_icon
* supersamples it 4x4 into a white alpha mask, drawn tinted to the wanted
* colour. one cached texture per icon (called at a fixed size each frame). */
static float seg_dist(float px, float py, float ax, float ay,
float bx, float by)
{
static vita2d_texture *cache;
static int cs;
if (cache && cs == s)
return cache;
if (cache) {
vita2d_wait_rendering_done();
vita2d_free_texture(cache);
cache = NULL;
float dx = bx - ax, dy = by - ay;
float l2 = dx * dx + dy * dy;
float t = l2 > 0 ? ((px - ax) * dx + (py - ay) * dy) / l2 : 0;
if (t < 0) t = 0;
if (t > 1) t = 1;
float qx = ax + dx * t, qy = ay + dy * t;
return sqrtf((px - qx) * (px - qx) + (py - qy) * (py - qy));
}
static int in_tri(float px, float py, float ax, float ay, float bx, float by,
float cx, float cy)
{
float d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by);
float d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy);
float d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay);
int neg = (d1 < 0) || (d2 < 0) || (d3 < 0);
int pos = (d1 > 0) || (d2 > 0) || (d3 > 0);
return !(neg && pos);
}
/* a thin ring outline with a straight, round-capped handle off the lower right */
static int cov_search(float px, float py, int s)
{
float R = s * 0.30f, W = s * 0.11f;
if (W < 1.8f) W = 1.8f;
float ccx = s * 0.40f, ccy = s * 0.40f, hw = W * 0.5f, c45 = 0.70710678f;
float dr = sqrtf((px - ccx) * (px - ccx) + (py - ccy) * (py - ccy));
int ring = (dr <= R && dr >= R - W);
float ax = ccx + c45 * (R - hw), ay = ccy + c45 * (R - hw);
float bx = ccx + c45 * (R + s * 0.42f), by = ccy + c45 * (R + s * 0.42f);
return ring || seg_dist(px, py, ax, ay, bx, by) <= hw;
}
/* a filled map marker: a round head (with a hole) tapering to a point */
static int cov_map(float px, float py, int s)
{
float cx = s * 0.5f, hcy = s * 0.40f, Rp = s * 0.30f, tipY = s * 0.95f;
float dHead = sqrtf((px - cx) * (px - cx) + (py - hcy) * (py - hcy));
int solid = (dHead <= Rp) ||
in_tri(px, py, cx - Rp * 0.78f, hcy + Rp * 0.30f,
cx + Rp * 0.78f, hcy + Rp * 0.30f, cx, tipY);
return solid && dHead >= Rp * 0.42f; /* punch the hole */
}
/* a filled cloud: three lobes tangent to a flat bottom (rounded sides), with
* a small body rectangle filling between the side lobes. all kept well inside
* the icon bounds so nothing clips at the edge. */
static int cov_cloud(float px, float py, int s)
{
float yb = s * 0.68f; /* flat bottom */
int mid = sqrtf((px - s * 0.50f) * (px - s * 0.50f) +
(py - s * 0.42f) * (py - s * 0.42f)) <= s * 0.22f;
int lp = sqrtf((px - s * 0.34f) * (px - s * 0.34f) +
(py - s * 0.52f) * (py - s * 0.52f)) <= s * 0.16f;
int rp = sqrtf((px - s * 0.66f) * (px - s * 0.66f) +
(py - s * 0.52f) * (py - s * 0.52f)) <= s * 0.16f;
int base = (px >= s * 0.34f && px <= s * 0.66f &&
py >= s * 0.52f && py <= yb);
return mid || lp || rp || base;
}
static vita2d_texture *bake_icon(int s, int (*inside)(float, float, int))
{
vita2d_texture *t = vita2d_create_empty_texture_format(s, s,
SCE_GXM_TEXTURE_FORMAT_A8B8G8R8);
if (!t)
@@ -1920,49 +2076,42 @@ static vita2d_texture *search_icon_tex(int s)
uint32_t *data = vita2d_texture_get_datap(t);
int stride = (int)(vita2d_texture_get_stride(t) / 4);
memset(data, 0, (size_t)stride * s * 4);
float R = s * 0.30f; /* ring outer radius */
float W = s * 0.11f; /* stroke width */
if (W < 1.8f) W = 1.8f;
float ccx = s * 0.40f, ccy = s * 0.40f; /* ring centre */
float hw = W * 0.5f;
float c45 = 0.70710678f;
/* handle segment, 45 deg toward the lower-right corner */
float ax = ccx + c45 * (R - hw), ay = ccy + c45 * (R - hw);
float bx = ccx + c45 * (R + s * 0.42f), by = ccy + c45 * (R + s * 0.42f);
float seg2 = (bx - ax) * (bx - ax) + (by - ay) * (by - ay);
for (int y = 0; y < s; y++) {
for (int y = 0; y < s; y++)
for (int x = 0; x < s; x++) {
int hits = 0;
for (int sy = 0; sy < 4; sy++)
for (int sx = 0; sx < 4; sx++) {
float px = x + (sx + 0.5f) / 4.0f;
float py = y + (sy + 0.5f) / 4.0f;
float dr = sqrtf((px - ccx) * (px - ccx) +
(py - ccy) * (py - ccy));
int in = (dr <= R && dr >= R - W);
/* distance to the handle segment */
float tt = seg2 > 0 ?
((px - ax) * (bx - ax) +
(py - ay) * (by - ay)) / seg2 : 0;
if (tt < 0) tt = 0;
if (tt > 1) tt = 1;
float hxp = ax + (bx - ax) * tt;
float hyp = ay + (by - ay) * tt;
float dh = sqrtf((px - hxp) * (px - hxp) +
(py - hyp) * (py - hyp));
if (in || dh <= hw)
hits++;
}
for (int sx = 0; sx < 4; sx++)
hits += inside(x + (sx + 0.5f) / 4.0f,
y + (sy + 0.5f) / 4.0f, s);
data[y * stride + x] = RGBA8(255, 255, 255,
hits * 255 / 16);
}
return t;
}
cache = t;
cs = s;
return cache;
/* 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[3];
static int cs[3];
if (which < 0 || which > 2)
return NULL;
if (cache[which] && cs[which] == s)
return cache[which];
if (cache[which]) {
vita2d_wait_rendering_done();
vita2d_free_texture(cache[which]);
cache[which] = NULL;
}
int (*fn)(float, float, int) = which == 0 ? cov_search :
which == 1 ? cov_map : cov_cloud;
cache[which] = bake_icon(s, fn);
cs[which] = s;
return cache[which];
}
#define ICON_SEARCH 0
#define ICON_MAP 1
#define ICON_CLOUD 2
/* small clear (x) chip used on the right of the bar while a search is on */
static void draw_clear_x(float cx, float cy, float s, unsigned int col)
@@ -1973,14 +2122,34 @@ static void draw_clear_x(float cx, float cy, float s, unsigned int col)
}
}
/* pixel geometry of the search pill, shared by the drawer and the tap test */
/* pixel geometry of the search pill + the two buttons on its right, shared
* by the drawer and the tap test */
#define BAR_M 12.0f /* left/right margin */
#define BAR_Y 8.0f /* top of the pill */
#define BAR_H 40.0f /* pill height */
#define BAR_R 12.0f /* corner radius */
#define BAR_W ((float)SCREEN_W - 2 * BAR_M)
#define BTN_SZ 40.0f /* button (rounded square) size */
#define BTN_GAP 10.0f /* gap between the two buttons */
#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 */
/* one future-feature button: rounded-square background + a centred AA glyph */
static void draw_bar_button(float cx, int icon)
{
vita2d_texture *bg = rounded_mask_tex((int)BTN_SZ, (int)BTN_SZ, 11.0f);
if (bg)
vita2d_draw_texture_tint(bg, cx - BTN_SZ / 2, BTN_CY - BTN_SZ / 2,
RGBA8(46, 46, 52, 255));
int isz = 26;
vita2d_texture *g = icon_tex(icon, isz);
if (g)
vita2d_draw_texture_tint(g, cx - isz / 2.0f, BTN_CY - isz / 2.0f,
RGBA8(190, 190, 196, 255));
}
static void draw_search_bar(void)
{
unsigned int field = RGBA8(38, 38, 42, 255);
@@ -1994,12 +2163,16 @@ static void draw_search_bar(void)
vita2d_draw_rectangle(BAR_M, BAR_Y, BAR_W, BAR_H, field);
int isz = 24;
vita2d_texture *icon = search_icon_tex(isz);
vita2d_texture *icon = icon_tex(ICON_SEARCH, isz);
if (icon)
vita2d_draw_texture_tint(icon, BAR_M + 22 - isz / 2.0f,
BAR_Y + BAR_H / 2 - isz / 2.0f,
RGBA8(190, 190, 196, 255));
/* placeholder buttons (map, cloud) for future features */
draw_bar_button(BTN1_CX, ICON_MAP);
draw_bar_button(BTN2_CX, ICON_CLOUD);
float tx = BAR_M + 44, ty = BAR_Y + BAR_H - 13;
if (g_search_active && g_search_query[0]) {
char q[64];
@@ -3510,39 +3683,11 @@ static int count_state(int st)
return n;
}
static const char *state_name(int st)
{
switch (st) {
case SYNC_UNSCANNED: return "not scanned";
case SYNC_HASHING: return "hashing";
case SYNC_CHECKING: return "checking server";
case SYNC_LOCAL_ONLY:return "local only";
case SYNC_QUEUED: return "queued";
case SYNC_UPLOADING: return "uploading";
case SYNC_BACKED_UP: return "backed up";
case SYNC_FAILED: return "failed";
default: return "?";
}
}
/* format a byte count as B / KB / MB / GB into `out` */
static void human_size(long long bytes, char *out, size_t len)
{
if (bytes >= 1024LL * 1024 * 1024)
snprintf(out, len, "%.1f GB", bytes / (1024.0 * 1024 * 1024));
else if (bytes >= 1024LL * 1024)
snprintf(out, len, "%.1f MB", bytes / (1024.0 * 1024));
else if (bytes >= 1024)
snprintf(out, len, "%.1f KB", bytes / 1024.0);
else
snprintf(out, len, "%lld B", bytes);
}
/* ------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------ */
enum { MODE_GRID, MODE_DETAIL, MODE_SYNC };
enum { MODE_GRID, MODE_DETAIL, MODE_CLOUD, MODE_CLOUD_DETAILS };
int main(void)
{
@@ -3651,8 +3796,8 @@ int main(void)
sceKernelStartThread(syncw, 0, NULL);
int mode = MODE_GRID;
int sync_return_mode = MODE_GRID;
int sel = 0;
int cloud_scroll = 0; /* first visible row on the upload-details list */
/* 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 */
@@ -3722,13 +3867,6 @@ int main(void)
}
}
/* SELECT toggles the sync overview from the grid / detail view */
if ((pressed & SCE_CTRL_SELECT) && mode != MODE_SYNC) {
sync_return_mode = mode;
mode = MODE_SYNC;
continue;
}
/* d-pad auto-repeat for fast scrolling */
const unsigned int dirs = SCE_CTRL_UP | SCE_CTRL_DOWN |
SCE_CTRL_LEFT | SCE_CTRL_RIGHT;
@@ -3744,6 +3882,7 @@ int main(void)
int do_search_open = (pressed & SCE_CTRL_TRIANGLE) != 0;
int do_search_clear = g_search_active &&
(pressed & SCE_CTRL_CIRCLE) != 0;
int do_open_cloud = 0; /* tap on the cloud button */
if (nav & SCE_CTRL_RIGHT)
sel++;
@@ -3887,10 +4026,18 @@ int main(void)
} else if (touch_active) {
touch_active = 0;
if (touch_on_bar) {
/* tap on the search bar: clear
* chip while a search is on, else
* open the keyboard */
if (g_search_active &&
/* tap on the bar: the map/cloud
* buttons are placeholders (no-op),
* the clear chip clears an active
* search, the field opens the
* keyboard */
if (touch_x > BTN2_CX - BTN_SZ / 2) {
/* cloud button: open the
* server/backup page */
do_open_cloud = 1;
} else if (touch_x > BAR_M + BAR_W) {
/* map button (placeholder) */
} else if (g_search_active &&
touch_x > BAR_CLEAR_CX - 16)
do_search_clear = 1;
else
@@ -3928,6 +4075,11 @@ int main(void)
}
}
if (do_open_cloud) {
mode = MODE_CLOUD;
continue;
}
/* act on a search request from TRIANGLE/CIRCLE
* or a tap on the bar. both block this frame
* (keyboard / request), so restart the loop. */
@@ -4141,7 +4293,7 @@ int main(void)
else
snprintf(hud, sizeof(hud),
"%d / %d%s %.10s X view /\\ search "
"SELECT sync START exit",
"START exit",
sel + 1, g_disp_count,
g_next_page > 0 ? "+" : "",
g_disp_count > 0 ? disp_date(sel) : "");
@@ -4589,142 +4741,214 @@ pf_skip:
vita2d_end_drawing();
vita2d_swap_buffers();
} else { /* MODE_SYNC */
if ((pressed & SCE_CTRL_CIRCLE) ||
(pressed & SCE_CTRL_SELECT)) {
mode = sync_return_mode;
detail_idx = -1; /* re-decode on return to detail */
} else if (mode == MODE_CLOUD) {
/* server + backup overview, opened from the cloud
* button on the search bar */
if (g_srv_state == 0) {
show_status("Loading server info...");
fetch_server_info();
}
int total = g_local_count;
int backed = count_state(SYNC_BACKED_UP);
int remain = total - backed;
/* on-screen action buttons (also X = details, /\ = upload) */
float bth = 46, bty = 404;
float dbx = 60, dbw = 360; /* See details */
float ubx = dbx + dbw + 20, ubw = 320; /* Upload all */
int go_details = (pressed & SCE_CTRL_CROSS) != 0;
int do_upload = (pressed & SCE_CTRL_TRIANGLE) != 0;
if (pressed & SCE_CTRL_CIRCLE) {
mode = MODE_GRID;
continue;
}
if (pressed & SCE_CTRL_CROSS) {
/* touch: tap the button to open the details list */
{
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;
touch_dragged = 0;
touch_start_x = tx;
touch_start_y = ty;
} else if (fabsf(tx - touch_start_x) > 12 ||
fabsf(ty - touch_start_y) > 12) {
touch_dragged = 1;
}
touch_x = tx;
touch_y = ty;
} else if (touch_active) {
touch_active = 0;
if (!touch_dragged && touch_y >= bty &&
touch_y <= bty + bth) {
if (touch_x >= dbx &&
touch_x <= dbx + dbw)
go_details = 1;
else if (touch_x >= ubx &&
touch_x <= ubx + ubw)
do_upload = 1;
}
}
}
if (do_upload) {
int q = queue_all_uploads();
log_line("sync: queued %d uploads", q);
log_line("cloud: queued %d uploads", q);
}
int backed = count_state(SYNC_BACKED_UP);
int local_only = count_state(SYNC_LOCAL_ONLY);
int queued = count_state(SYNC_QUEUED) +
count_state(SYNC_UPLOADING);
int failed = count_state(SYNC_FAILED);
int pending = count_state(SYNC_UNSCANNED) +
count_state(SYNC_HASHING) +
count_state(SYNC_CHECKING);
/* photo/video split + total and backed-up byte totals */
int srv_vid = 0;
for (int i = 0; i < g_asset_count; i++)
srv_vid += g_asset_is_video[i] ? 1 : 0;
int loc_vid = 0;
long long loc_bytes = 0, backed_bytes = 0;
for (int j = 0; j < g_local_count; j++) {
loc_vid += g_local_is_video[j] ? 1 : 0;
loc_bytes += g_local_size[j];
if (g_local_state[j] == SYNC_BACKED_UP)
backed_bytes += g_local_size[j];
if (go_details) {
mode = MODE_CLOUD_DETAILS;
cloud_scroll = 0;
continue;
}
int loc_pct = g_local_count ? (backed * 100 / g_local_count) : 0;
vita2d_start_drawing();
vita2d_clear_screen();
draw_centered(40, RGBA8(255, 255, 255, 255),
"Server & Backup");
draw_centered(40, RGBA8(255, 255, 255, 255), "Sync overview");
char line[200], sz1[24], sz2[24];
int y = 84;
char line[300];
uint32_t c = RGBA8(220, 220, 220, 255);
snprintf(line, sizeof(line),
"Server assets: %d%s (%d photos, %d videos)",
g_asset_count, g_next_page > 0 ? "+" : "",
g_asset_count - srv_vid, srv_vid);
draw_text(60, y, c, 1.0f, line); y += 30;
human_size(loc_bytes, sz1, sizeof(sz1));
snprintf(line, sizeof(line),
"Local files: %d (%d photos, %d videos) %s",
g_local_count, g_local_count - loc_vid, loc_vid, sz1);
draw_text(60, y, c, 1.0f, line); y += 30;
snprintf(line, sizeof(line),
"backed up %d local-only %d queued/uploading %d",
backed, local_only, queued);
draw_text(60, y, c, 1.0f, line); y += 30;
snprintf(line, sizeof(line), "scanning/hashing %d failed %d",
pending, failed);
draw_text(60, y, c, 1.0f, line); y += 30;
human_size(backed_bytes, sz1, sizeof(sz1));
human_size(loc_bytes, sz2, sizeof(sz2));
snprintf(line, sizeof(line),
"Backed up: %d%% of local (%s / %s)",
loc_pct, sz1, sz2);
draw_text(60, y,
RGBA8(120, 200, 120, 255), 1.0f, line);
y += 38;
uint32_t dim = RGBA8(150, 150, 155, 255);
int y = 92;
/* details of the item currently selected in the grid */
if (g_disp_count > 0 && sel >= 0 && sel < g_disp_count) {
struct disp_item *si = &g_disp[sel];
const char *name;
if (si->src == SRC_LOCAL) {
const char *p = g_local_path[si->idx];
/* server storage with a usage bar */
if (g_srv_state == 1 && g_srv_total[0]) {
snprintf(line, sizeof(line),
"Server storage: %s of %s used (%d%%)",
g_srv_use[0] ? g_srv_use : "?",
g_srv_total, g_srv_pct);
draw_text(60, y, c, 1.0f, line);
y += 24;
float bw = 600, bh = 14;
vita2d_draw_rectangle(60, y, bw, bh,
RGBA8(48, 48, 54, 255));
float f = g_srv_pct / 100.0f;
if (f < 0) f = 0;
if (f > 1) f = 1;
vita2d_draw_rectangle(60, y, bw * f, bh,
RGBA8(94, 110, 215, 255));
y += 40;
} else {
draw_text(60, y, dim, 1.0f,
"Server storage: unavailable");
y += 40;
}
snprintf(line, sizeof(line), "Server version: %s",
g_srv_version[0] ? g_srv_version : "unknown");
draw_text(60, y, c, 1.0f, line); y += 30;
snprintf(line, sizeof(line), "Server URL: %.48s", g_server);
draw_text(60, y, c, 1.0f, line); y += 30;
snprintf(line, sizeof(line), "App version: %s", APP_VERSION);
draw_text(60, y, c, 1.0f, line); y += 44;
draw_text(60, y, RGBA8(255, 255, 255, 255), 1.0f,
"On this PS Vita"); y += 30;
snprintf(line, sizeof(line), " Images detected: %d", total);
draw_text(60, y, c, 1.0f, line); y += 28;
snprintf(line, sizeof(line), " Backed up: %d", backed);
draw_text(60, y, RGBA8(120, 200, 120, 255), 1.0f, line);
y += 28;
snprintf(line, sizeof(line), " Remaining: %d", remain);
draw_text(60, y, RGBA8(220, 180, 120, 255), 1.0f, line);
/* See details + Upload all buttons */
vita2d_texture *db = rounded_mask_tex((int)dbw, (int)bth, 10.0f);
if (db)
vita2d_draw_texture_tint(db, dbx, bty,
RGBA8(46, 46, 52, 255));
else
vita2d_draw_rectangle(dbx, bty, dbw, bth,
RGBA8(46, 46, 52, 255));
snprintf(line, sizeof(line), "See details (%d remaining)", remain);
draw_text(dbx + 18, bty + bth - 16,
RGBA8(230, 230, 235, 255), 0.95f, line);
vita2d_texture *ub = rounded_mask_tex((int)ubw, (int)bth, 10.0f);
/* the upload button reads as active (indigo) when there's
* something to send, dimmed when everything is backed up */
uint32_t uc = remain > 0 ? RGBA8(94, 110, 215, 255)
: RGBA8(46, 46, 52, 255);
if (ub)
vita2d_draw_texture_tint(ub, ubx, bty, uc);
else
vita2d_draw_rectangle(ubx, bty, ubw, bth, uc);
draw_text(ubx + 18, bty + bth - 16,
RGBA8(245, 245, 250, 255), 0.95f, "Upload all");
draw_hud("X / tap details /\\ / tap upload all O back START exit");
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) {
mode = MODE_CLOUD;
continue;
}
const int top = 86, rowh = 28, rows = (SCREEN_H - top - 40) / rowh;
int total_unbacked = g_local_count - count_state(SYNC_BACKED_UP);
int maxscroll = total_unbacked - rows;
if (maxscroll < 0)
maxscroll = 0;
if (nav & SCE_CTRL_DOWN)
cloud_scroll++;
if (nav & SCE_CTRL_UP)
cloud_scroll--;
if (cloud_scroll > maxscroll)
cloud_scroll = maxscroll;
if (cloud_scroll < 0)
cloud_scroll = 0;
vita2d_start_drawing();
vita2d_clear_screen();
char line[300];
snprintf(line, sizeof(line), "Not backed up (%d)",
total_unbacked);
draw_centered(40, RGBA8(255, 255, 255, 255), line);
/* walk local files, skipping backed-up ones, render the
* window [cloud_scroll, cloud_scroll+rows) */
int seen = 0, drawn = 0, y = top;
for (int j = 0; j < g_local_count && drawn < rows; j++) {
if (g_local_state[j] == SYNC_BACKED_UP)
continue;
if (seen++ < cloud_scroll)
continue;
const char *p = g_local_path[j];
const char *slash = strrchr(p, '/');
name = slash ? slash + 1 : p;
} else {
name = g_asset_ids[si->idx];
const char *name = slash ? slash + 1 : p;
int st = g_local_state[j];
const char *stx = st == SYNC_HASHING ? "hashing" :
st == SYNC_UPLOADING ? "uploading" :
st == SYNC_FAILED ? "failed" :
"waiting";
uint32_t sc = st == SYNC_UPLOADING ?
RGBA8(120, 200, 120, 255) :
st == SYNC_HASHING ?
RGBA8(220, 210, 120, 255) :
st == SYNC_FAILED ?
RGBA8(220, 120, 120, 255) :
RGBA8(170, 170, 178, 255);
snprintf(line, sizeof(line), "%.46s", name);
draw_text(40, y, RGBA8(220, 220, 220, 255),
0.9f, line);
draw_text(SCREEN_W - 170, y, sc, 0.9f, stx);
y += rowh;
drawn++;
}
snprintf(line, sizeof(line), "Selected: %.40s", name);
draw_text(60, y, c, 0.95f, line);
y += 26;
if (si->src == SRC_LOCAL) {
human_size(g_local_size[si->idx], sz1,
sizeof(sz1));
snprintf(line, sizeof(line),
" %.19s %s %s%s",
g_local_date[si->idx], sz1,
g_local_is_video[si->idx] ?
"video, " : "photo, ",
state_name(g_local_state[si->idx]));
} else {
snprintf(line, sizeof(line),
" %.19s %s on server",
g_asset_dates[si->idx],
g_asset_is_video[si->idx] ?
"video" : "photo");
}
draw_text(60, y, c, 0.95f, line);
}
y += 36;
/* current activity + upload progress */
if (g_sync_activity[0]) {
if (g_sync_phase == 3 && g_ul_total > 0) {
float frac = (float)g_ul_now / (float)g_ul_total;
snprintf(line, sizeof(line), "%s %.0f%%",
g_sync_activity, frac * 100.0f);
} else {
snprintf(line, sizeof(line), "%s",
g_sync_activity);
}
draw_text(60, y,
if (total_unbacked == 0)
draw_centered(SCREEN_H / 2,
RGBA8(120, 200, 120, 255),
1.0f, line);
}
y += 44;
if (g_sync_errn > 0) {
draw_text(60, y,
RGBA8(220, 120, 120, 255),
1.0f, "Recent errors:");
y += 30;
int shown = g_sync_errn < 5 ? g_sync_errn : 5;
for (int k = 0; k < shown && y < 480; k++) {
int e = (g_sync_errn - shown + k) % 5;
draw_text(60, y,
RGBA8(200, 160, 160, 255),
0.9f, g_sync_errlog[e]);
y += 26;
}
}
draw_hud("X upload all local-only O / SELECT back START exit");
"Everything is backed up");
draw_hud("Up/Down scroll O back START exit");
vita2d_end_drawing();
vita2d_swap_buffers();
}