Albums: name search and sort by name / modification date

The album grid now shows a filtered + sorted view of the list:
- SQUARE (or tap the header pill) opens the keyboard to filter albums
  by name (case-insensitive); O clears the filter before going back.
- SELECT (or tap the chip) cycles the order: modified newest/oldest,
  name A-Z/Z-A. Uses the album's updatedAt from GET /api/albums.
- Cover eviction no longer drops a texture that a visible card still
  uses when two albums share the same cover photo.
- README: albums controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nino
2026-10-23 17:25:42 -06:00
co-authored by Claude Opus 5.5
parent 07890191cf
commit c296ff5ade
7 changed files with 275 additions and 46 deletions
+240 -43
View File
@@ -9,7 +9,11 @@
/* albums page */
/* */
/* Opened from the albums button on the top bar. A grid of album cards */
/* (cover, name, item count). Covers are ordinary server thumbnails: */
/* (cover, name, item count). The grid shows a VIEW of the list: */
/* g_alb_view holds g_album_* indices filtered by the name search */
/* (SQUARE / tap the pill) and sorted by name or last modification */
/* (SELECT / tap the chip). g_alb_sel is a position in that view. */
/* Covers are ordinary server thumbnails: */
/* each album's cover id becomes a g_asset entry (asset_for_id) and the */
/* shared thumbnail worker loads it into g_thumb[]. Only rows near the */
/* viewport keep their cover texture (libraries can have hundreds of */
@@ -26,19 +30,114 @@
#define ALB_X0 ((SCREEN_W - ALB_COLS * ALB_CW - (ALB_COLS - 1) * ALB_GAP) / 2)
#define ALB_GLYPH 44 /* album glyph bake size on this page */
enum { ALB_NONE, ALB_BACK, ALB_OPEN, ALB_REFRESH };
/* header controls: the search pill and the sort chip */
#define ALB_HY 9.0f /* top of pill + chip */
#define ALB_HH 38.0f /* their height */
#define ALB_SX 190.0f /* search pill */
#define ALB_SW 380.0f
#define ALB_OX (ALB_SX + ALB_SW + 14.0f) /* sort chip */
#define ALB_OW 230.0f
static int g_alb_sel; /* selected album */
enum { ALB_NONE, ALB_BACK, ALB_OPEN, ALB_REFRESH, ALB_SEARCH };
/* sort orders, cycled by SELECT */
enum { ALB_SORT_MOD_NEW, ALB_SORT_MOD_OLD, ALB_SORT_NAME_AZ, ALB_SORT_NAME_ZA,
ALB_NSORT };
static const char *const alb_sort_label[ALB_NSORT] = {
"Modified: newest", "Modified: oldest", "Name: A-Z", "Name: Z-A",
};
static int g_alb_sel; /* selected position in the view */
static float g_alb_scroll, g_alb_target;
static int g_alb_touch, g_alb_dragged;
static float g_alb_tx, g_alb_ty, g_alb_t0y, g_alb_s0;
static int g_alb_sort; /* ALB_SORT_* */
static char g_alb_query[128]; /* name filter, "" = all albums */
static int *g_alb_view; /* g_album_* indices, filtered+sorted */
static int g_alb_vlen, g_alb_vcap;
static float alb_content_h(void)
{
int rows = (g_album_len + ALB_COLS - 1) / ALB_COLS;
int rows = (g_alb_vlen + ALB_COLS - 1) / ALB_COLS;
return ALB_TOP + rows * ALB_ROWH;
}
/* ASCII case-insensitive substring test (other UTF-8 bytes match exactly) */
static int alb_name_match(const char *name, const char *q)
{
if (!q[0])
return 1;
for (; *name; name++) {
const char *a = name, *b = q;
while (*a && *b && tolower((unsigned char)*a) ==
tolower((unsigned char)*b)) {
a++;
b++;
}
if (!*b)
return 1;
}
return 0;
}
static int alb_cmp(const void *pa, const void *pb)
{
int a = *(const int *)pa, b = *(const int *)pb, r = 0;
switch (g_alb_sort) {
case ALB_SORT_MOD_NEW:
r = strcmp(g_album_updated[b], g_album_updated[a]);
break;
case ALB_SORT_MOD_OLD:
r = strcmp(g_album_updated[a], g_album_updated[b]);
break;
case ALB_SORT_NAME_AZ:
r = strcasecmp(g_album_name[a], g_album_name[b]);
break;
case ALB_SORT_NAME_ZA:
r = strcasecmp(g_album_name[b], g_album_name[a]);
break;
}
return r ? r : a - b; /* ties keep the server's order */
}
/* rebuild g_alb_view from the list, the query and the sort order. the
* selected album stays selected when it is still in the view (a refresh);
* callers wanting the first card set g_alb_sel = -1 first. */
static void albums_rebuild_view(void)
{
int keep = (g_alb_sel >= 0 && g_alb_sel < g_alb_vlen) ?
g_alb_view[g_alb_sel] : -1;
g_album_list_dirty = 0;
g_alb_vlen = 0;
if (g_album_len > g_alb_vcap) {
int cap = GROW(g_alb_view, g_alb_vcap, g_album_len);
if (!cap)
return;
g_alb_vcap = cap;
}
for (int a = 0; a < g_album_len; a++)
if (alb_name_match(g_album_name[a], g_alb_query))
g_alb_view[g_alb_vlen++] = a;
qsort(g_alb_view, g_alb_vlen, sizeof(g_alb_view[0]), alb_cmp);
g_alb_sel = 0;
for (int v = 0; v < g_alb_vlen; v++)
if (g_alb_view[v] == keep)
g_alb_sel = v;
log_line("albums view: %d of %d (sort %d, query \"%s\")", g_alb_vlen,
g_album_len, g_alb_sort, g_alb_query);
}
/* new name filter (from the keyboard); "" shows every album */
static void albums_set_query(const char *q)
{
snprintf(g_alb_query, sizeof(g_alb_query), "%s", q);
g_alb_sel = -1; /* start at the first match */
albums_rebuild_view();
g_alb_scroll = g_alb_target = 0;
}
static void alb_card_xy(int a, float *x, float *y)
{
*x = ALB_X0 + (a % ALB_COLS) * (ALB_CW + ALB_GAP);
@@ -89,12 +188,22 @@ static void albums_pump_covers(int first, int last)
/* rows more than one row outside the viewport drop their texture */
int keep0 = first - ALB_COLS, keep1 = last + ALB_COLS;
for (int a = 0; a < g_album_len; a++) {
if (a >= keep0 && a <= keep1)
if (keep0 < 0)
keep0 = 0;
if (keep1 >= g_alb_vlen)
keep1 = g_alb_vlen - 1;
for (int v = 0; v < g_alb_vlen; v++) {
if (v >= keep0 && v <= keep1)
continue;
int idx = g_album_cover[a];
if (idx >= 0 && g_thumb[idx] &&
!(g_req_state != REQ_IDLE && g_req_idx == idx)) {
int idx = g_album_cover[g_alb_view[v]];
if (idx < 0 || !g_thumb[idx] ||
(g_req_state != REQ_IDLE && g_req_idx == idx))
continue;
/* albums can share a cover photo: keep it if a kept card uses it */
int shared = 0;
for (int k = keep0; k <= keep1 && !shared; k++)
shared = g_album_cover[g_alb_view[k]] == idx;
if (!shared) {
tex_release(g_thumb[idx]);
g_thumb[idx] = NULL;
}
@@ -103,8 +212,11 @@ static void albums_pump_covers(int first, int last)
if (g_req_state != REQ_IDLE)
return;
for (int k = -1; k <= last - first; k++) {
int a = (k < 0) ? g_alb_sel : first + k; /* selection first */
if (a < 0 || a >= g_album_len || !g_album_thumb[a][0])
int v = (k < 0) ? g_alb_sel : first + k; /* selection first */
if (v < 0 || v >= g_alb_vlen)
continue;
int a = g_alb_view[v];
if (!g_album_thumb[a][0])
continue;
if (g_album_cover[a] < 0)
g_album_cover[a] = asset_for_id(g_album_thumb[a]);
@@ -160,14 +272,89 @@ static void draw_album_card(int a, float x, float y, int selected)
draw_text(x, y + ALB_COVH + 44, RGBA8(140, 140, 148, 255), 0.8f, meta);
}
/* header: search pill (query or placeholder) and the sort chip */
static void draw_albums_header(int n)
{
vita2d_draw_rectangle(0, 0, SCREEN_W, ALB_TOP - 8, RGBA8(16, 16, 16, 255));
int gs = 24;
vita2d_texture *og = icon_tex(ICON_CIRCLE, gs);
if (og)
vita2d_draw_texture_tint(og, 18, 16, RGBA8(235, 90, 85, 255));
/* same bake size as the card placeholder: icon_tex caches one
* size per glyph, so mixing sizes in a frame re-bakes (and frees)
* the texture mid-frame */
vita2d_texture *ag = icon_tex(ICON_ALBUM, ALB_GLYPH);
if (ag)
vita2d_draw_texture_tint_scale(ag, 56, 14, 28.0f / ALB_GLYPH,
28.0f / ALB_GLYPH,
RGBA8(235, 235, 240, 255));
draw_text(94, 37, RGBA8(235, 235, 240, 255), 1.1f, "Albums");
uint32_t field = RGBA8(38, 38, 42, 255);
uint32_t dim = RGBA8(140, 140, 148, 255);
uint32_t txt = RGBA8(235, 235, 240, 255);
float ty = ALB_HY + ALB_HH - 12;
/* search pill: [search] query ... [] (or O to clear) */
vita2d_texture *pm = rounded_mask_tex((int)ALB_SW, (int)ALB_HH, 10.0f);
if (pm)
vita2d_draw_texture_tint(pm, ALB_SX, ALB_HY, field);
else
vita2d_draw_rectangle(ALB_SX, ALB_HY, ALB_SW, ALB_HH, field);
vita2d_texture *si = icon_tex(ICON_SEARCH, 24);
if (si)
vita2d_draw_texture_tint(si, ALB_SX + 10, ALB_HY + (ALB_HH - 24) / 2,
RGBA8(190, 190, 196, 255));
char line[96];
if (g_alb_query[0]) {
alb_fit(line, sizeof(line), g_alb_query, 0.9f, (int)ALB_SW - 84);
draw_text(ALB_SX + 42, ty, txt, 0.9f, line);
if (og) /* O clears the filter */
vita2d_draw_texture_tint(og, ALB_SX + ALB_SW - 34,
ALB_HY + (ALB_HH - gs) / 2,
RGBA8(235, 90, 85, 255));
} else {
draw_text(ALB_SX + 42, ty, dim, 0.9f, "Search albums");
}
if (!g_alb_query[0]) {
vita2d_texture *sq = icon_tex(ICON_SQUARE, 20);
if (sq)
vita2d_draw_texture_tint(sq, ALB_SX + ALB_SW - 32,
ALB_HY + (ALB_HH - 20) / 2,
RGBA8(232, 120, 175, 255));
}
/* sort chip: SELECT + the current order */
vita2d_texture *cm = rounded_mask_tex((int)ALB_OW, (int)ALB_HH, 10.0f);
if (cm)
vita2d_draw_texture_tint(cm, ALB_OX, ALB_HY, field);
else
vita2d_draw_rectangle(ALB_OX, ALB_HY, ALB_OW, ALB_HH, field);
draw_text(ALB_OX + 12, ty - 1, dim, 0.7f, "SELECT");
draw_text(ALB_OX + 12 + text_width(0.7f, "SELECT") + 10, ty, txt, 0.9f,
alb_sort_label[g_alb_sort]);
char cnt[64];
if (g_alb_query[0])
snprintf(cnt, sizeof(cnt), "%d of %d", n, g_album_len);
else
snprintf(cnt, sizeof(cnt), "%d album%s", n, n == 1 ? "" : "s");
draw_text(SCREEN_W - 18 - text_width(0.8f, cnt), 36, dim, 0.8f, cnt);
}
/* one frame of the album list: input, cover loading and drawing. returns
* ALB_OPEN (open g_alb_sel), ALB_BACK, ALB_REFRESH or ALB_NONE. */
* ALB_OPEN (open view position g_alb_sel), ALB_BACK, ALB_REFRESH,
* ALB_SEARCH (bring up the keyboard) or ALB_NONE. */
static int albums_page(unsigned int pressed, unsigned int nav)
{
int action = ALB_NONE;
int n = g_album_len;
if (g_album_list_dirty)
albums_rebuild_view();
int n = g_alb_vlen;
if (g_alb_sel >= n)
g_alb_sel = n > 0 ? n - 1 : 0;
if (g_alb_sel < 0)
g_alb_sel = 0;
if (n > 0) {
if (nav & SCE_CTRL_RIGHT && g_alb_sel + 1 < n)
@@ -183,10 +370,17 @@ static int albums_page(unsigned int pressed, unsigned int nav)
if (pressed & SCE_CTRL_CROSS)
action = ALB_OPEN;
}
if (pressed & SCE_CTRL_CIRCLE)
action = ALB_BACK;
int do_sort = (pressed & SCE_CTRL_SELECT) != 0;
if (pressed & SCE_CTRL_CIRCLE) {
if (g_alb_query[0])
albums_set_query(""); /* O first clears the filter */
else
action = ALB_BACK;
}
if (pressed & SCE_CTRL_TRIANGLE)
action = ALB_REFRESH;
if (pressed & SCE_CTRL_SQUARE)
action = ALB_SEARCH;
float maxs = alb_content_h() - SCREEN_H + 16;
if (maxs < 0)
@@ -216,8 +410,18 @@ static int albums_page(unsigned int pressed, unsigned int nav)
g_alb_ty = ty;
} else if (g_alb_touch) {
g_alb_touch = 0;
if (!g_alb_dragged && g_alb_ty < ALB_TOP - 8 && g_alb_tx < 140) {
action = ALB_BACK; /* tap on the header's back label */
if (!g_alb_dragged && g_alb_ty < ALB_TOP - 8) {
/* header taps: back label, search pill (its O end
* clears an active filter), sort chip */
if (g_alb_tx < 140)
action = ALB_BACK;
else if (g_alb_tx >= ALB_SX && g_alb_tx < ALB_SX + ALB_SW) {
if (g_alb_query[0] && g_alb_tx > ALB_SX + ALB_SW - 48)
albums_set_query("");
else
action = ALB_SEARCH;
} else if (g_alb_tx >= ALB_OX && g_alb_tx < ALB_OX + ALB_OW)
do_sort = 1;
} else if (!g_alb_dragged) {
float cy = g_alb_ty + g_alb_scroll - ALB_TOP;
int row = (int)(cy / ALB_ROWH);
@@ -234,6 +438,13 @@ static int albums_page(unsigned int pressed, unsigned int nav)
}
}
}
if (do_sort) {
/* a new order starts from its top */
g_alb_sort = (g_alb_sort + 1) % ALB_NSORT;
g_alb_sel = -1;
albums_rebuild_view();
g_alb_scroll = g_alb_target = 0;
}
/* keep the selected card on screen (d-pad), with an eased scroll */
if (!g_alb_touch) {
@@ -263,37 +474,23 @@ static int albums_page(unsigned int pressed, unsigned int nav)
vita2d_start_drawing();
vita2d_clear_screen();
vita2d_draw_rectangle(0, 0, SCREEN_W, SCREEN_H, RGBA8(16, 16, 16, 255));
for (int a = first; a <= last && a < n; a++) {
for (int v = first; v <= last && v < n; v++) {
float x, y;
alb_card_xy(a, &x, &y);
draw_album_card(a, x, y - g_alb_scroll, a == g_alb_sel);
alb_card_xy(v, &x, &y);
draw_album_card(g_alb_view[v], x, y - g_alb_scroll,
v == g_alb_sel);
}
if (n == 0)
if (n == 0 && g_alb_query[0]) {
char msg[160];
snprintf(msg, sizeof(msg), "No albums match \"%.64s\"", g_alb_query);
draw_centered(SCREEN_H / 2, RGBA8(160, 160, 165, 255), msg);
} else if (n == 0) {
draw_centered(SCREEN_H / 2, RGBA8(160, 160, 165, 255),
"No albums on this account");
/* header: back label, title, count (drawn last: cards scroll under it) */
vita2d_draw_rectangle(0, 0, SCREEN_W, ALB_TOP - 8, RGBA8(16, 16, 16, 255));
{
int gs = 24;
vita2d_texture *og = icon_tex(ICON_CIRCLE, gs);
if (og)
vita2d_draw_texture_tint(og, 18, 16, RGBA8(235, 90, 85, 255));
/* same bake size as the card placeholder: icon_tex caches one
* size per glyph, so mixing sizes in a frame re-bakes (and frees)
* the texture mid-frame */
vita2d_texture *ag = icon_tex(ICON_ALBUM, ALB_GLYPH);
if (ag)
vita2d_draw_texture_tint_scale(ag, 56, 14, 28.0f / ALB_GLYPH,
28.0f / ALB_GLYPH,
RGBA8(235, 235, 240, 255));
draw_text(94, 37, RGBA8(235, 235, 240, 255), 1.1f, "Albums");
char cnt[64];
snprintf(cnt, sizeof(cnt), "%d album%s X open /\\ refresh",
n, n == 1 ? "" : "s");
draw_text(SCREEN_W - 18 - text_width(0.8f, cnt), 36,
RGBA8(140, 140, 148, 255), 0.8f, cnt);
}
/* drawn last: cards scroll under it */
draw_albums_header(n);
vita2d_end_drawing();
vita2d_swap_buffers();
return action;
+9
View File
@@ -468,6 +468,7 @@ static int parse_albums(const char *js, size_t len)
g_album_nassets[cur] = 0;
g_album_shared[cur] = 0;
g_album_has_link[cur] = 0;
g_album_updated[cur][0] = '\0';
g_album_cover[cur] = -1;
continue;
}
@@ -501,6 +502,11 @@ static int parse_albums(const char *js, size_t len)
g_album_shared[cur] = (unsigned char)json_true(js, v);
else if (kl == 13 && !strncmp(k, "hasSharedLink", 13))
g_album_has_link[cur] = (unsigned char)json_true(js, v);
else if (kl == 9 && !strncmp(k, "updatedAt", 9) &&
v->type == JSMN_STRING &&
vl < (int)sizeof(g_album_updated[0]))
snprintf(g_album_updated[cur], sizeof(g_album_updated[0]),
"%.*s", vl, js + v->start);
}
free(t);
@@ -516,6 +522,8 @@ static int parse_albums(const char *js, size_t len)
g_album_nassets[w] = g_album_nassets[r];
g_album_shared[w] = g_album_shared[r];
g_album_has_link[w] = g_album_has_link[r];
memcpy(g_album_updated[w], g_album_updated[r],
sizeof(g_album_updated[0]));
g_album_cover[w] = g_album_cover[r];
}
w++;
@@ -553,6 +561,7 @@ static void fetch_albums(void)
return;
}
log_line("albums: %d fetched", n);
g_album_list_dirty = 1;
__sync_synchronize();
g_album_state = 1;
}
+9 -2
View File
@@ -191,6 +191,8 @@ static void reset_for_account_change(void)
g_album_fetching = 0;
g_alb_sel = 0;
g_alb_scroll = g_alb_target = 0;
g_alb_vlen = 0;
g_alb_query[0] = '\0';
g_srv_state = 0; /* re-fetch the cloud page's server info */
g_srv_fetching = 0;
g_map_state = 0; /* re-fetch the new account's map markers */
@@ -1992,7 +1994,12 @@ pf_skip:
} else if (act == ALB_REFRESH) {
albums_release_covers();
g_album_state = 0;
} else if (act == ALB_OPEN && g_alb_sel < g_album_len) {
} else if (act == ALB_SEARCH) {
char q[128] = "";
if (ime_input("Search albums", g_alb_query, q,
sizeof(q), 0))
albums_set_query(q);
} else if (act == ALB_OPEN && g_alb_sel < g_alb_vlen) {
/* fetch the members on a worker thread; the main
* thread only animates the throbber meanwhile */
albums_release_covers();
@@ -2003,7 +2010,7 @@ pf_skip:
g_search_active = 0;
if (g_gallery_active)
gallery_close();
g_album_open = g_alb_sel;
g_album_open = g_alb_view[g_alb_sel];
g_album_assets_done = 0;
SceUID ot = sceKernelCreateThread("album_open",
album_open_thread, 0x10000100,
+2
View File
@@ -17,6 +17,8 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <stdarg.h>
#include <math.h>
#include <setjmp.h>
+3
View File
@@ -140,11 +140,13 @@ static char (*g_album_thumb)[40]; /* cover asset id, "" when none */
static int *g_album_nassets; /* assetCount reported by the server */
static unsigned char *g_album_shared; /* shared with other users */
static unsigned char *g_album_has_link; /* has a public share link */
static char (*g_album_updated)[32]; /* updatedAt (ISO 8601, sorts as text) */
static int g_album_len; /* number of albums fetched */
static int g_album_cap;
static int g_album_state;
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 */
static int g_album_open = -1; /* index into g_album_* being shown */
@@ -301,6 +303,7 @@ static int grow_albums(int need)
!GROW(g_album_nassets, c, need) ||
!GROW(g_album_shared, c, need) ||
!GROW(g_album_has_link, c, need) ||
!GROW(g_album_updated, c, need) ||
!(g_album_cap = GROW(g_album_cover, c, need)))
return 0;
return 1;
+1 -1
View File
@@ -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 = 14 }; /* a small fixed set of shapes (pill, buttons, ...) */
enum { NSLOT = 20 }; /* a small fixed set of shapes (pill, buttons, ...) */
static vita2d_texture *cache[NSLOT];
static int cw[NSLOT], ch[NSLOT];
static float cr[NSLOT];