Files
vitaImmich/src/state.c
T
ninoandClaude Opus 5.5 c296ff5ade 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>
2026-10-23 17:25:42 -06:00

337 lines
13 KiB
C

/* 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.
*/
/* ------------------------------------------------------------------ */
/* local camera media + sync state */
/* ------------------------------------------------------------------ */
/* per-item sync state machine. transitions are written by the sync
* thread, except QUEUED which the main thread sets on a LOCAL_ONLY item
* (guarded the same way as the thumb worker's g_req_state handoff). */
enum {
SYNC_UNSCANNED, /* found on disk, not hashed yet */
SYNC_HASHING, /* sync thread is computing SHA1 */
SYNC_CHECKING, /* hashed, awaiting/under bulk-upload-check */
SYNC_LOCAL_ONLY, /* not on the server */
SYNC_QUEUED, /* user asked to upload (main-thread write) */
SYNC_UPLOADING, /* sync thread is POSTing it */
SYNC_BACKED_UP, /* exists on the server */
SYNC_FAILED, /* upload/check error (see g_local_err) */
};
/* local camera-media arrays. grown (doubling) during the one-shot startup
* scan, then fixed for the rest of the run, so the sync thread can read them
* without a realloc moving the storage out from under it. */
static char (*g_local_path)[256];
static char (*g_local_date)[DATELEN]; /* sortable mtime */
static char (*g_local_sha1)[41]; /* lowercase hex */
static char (*g_local_server_id)[40]; /* assetId once backed up */
static char (*g_local_err)[96];
static SceDateTime *g_local_mtime;
static SceDateTime *g_local_ctime;
static long long *g_local_size;
static unsigned char *g_local_is_video;
static volatile int *g_local_state;
static int g_local_count;
static int g_local_cap;
static vita2d_texture **g_local_thumb;
static int *g_local_thumb_failed;
/* merged, date-desc display order of server + local items */
enum { SRC_SERVER, SRC_LOCAL };
struct disp_item { unsigned char src; int idx; };
static struct disp_item *g_disp;
static int g_disp_count;
static int g_disp_cap;
/* per-display-slot grid layout (filled by layout_grid): pixel position and
* width/height of the cell (justified rows: width follows the photo's
* aspect, the row is stretched to span the full screen width), plus the
* month/year section headers above each month's run */
static float *g_item_x;
static float *g_item_y;
static float *g_item_w;
static float *g_item_h;
struct sect_hdr { float y; char label[24]; };
static struct sect_hdr *g_sect;
static int g_sect_count;
static int g_sect_cap;
static float g_content_h; /* total scrollable height incl. headers */
/* map cluster gallery: the photos of one map bubble, shown on the SAME grid as
* the timeline/search — a filtered view like g_search_*. g_gallery_idx holds
* indices into the g_asset_* arrays for the bubble's members; members not
* already paged into the library are appended as g_asset entries flagged
* g_asset_hidden (so they appear only here, never in the main timeline). Their
* date/aspect are resolved lazily by the metadata worker below, so the grid
* reflows into real month sections + true aspect ratios as they arrive. */
static int g_gallery_active;
static int *g_gallery_idx;
static int g_gallery_count;
static int g_gallery_cap;
/* background metadata resolver: a one-slot handoff (mirroring the thumb worker)
* that fetches one asset's date/aspect so the justified grid can place it. */
enum { META_IDLE, META_PENDING, META_DONE };
static volatile int g_meta_state = META_IDLE;
static char g_meta_id[40]; /* asset id to resolve (main -> worker) */
static int g_meta_target; /* g_asset index to fill (main-thread owned) */
static char g_meta_date[DATELEN]; /* results (worker -> main) */
static float g_meta_ratio;
static unsigned char g_meta_rot;
static int g_meta_is_video;
/* free-text smart (CLIP) search. when active the grid shows only the matching
* server assets — still date-grouped under month headers like the timeline —
* instead of the full merged library; TRIANGLE opens the keyboard, CIRCLE (or
* the bar's clear chip) returns to the timeline. g_search_idx holds indices
* into the g_asset_* arrays (those never reorder, only grow), in the order the
* server returned them. */
static int g_search_active;
static char g_search_query[128];
static int *g_search_idx;
static int g_search_count;
static int g_search_cap;
/* smart search runs on a worker thread so the grid can animate a throbber
* while the request is in flight; g_search_pending is the query it runs,
* g_search_done/g_search_result report completion + its return value */
static char g_search_pending[128];
static volatile int g_search_done;
static volatile int g_search_result;
/* 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.
* g_srv_fetching: a worker thread is fetching it now (so the page animates a
* throbber instead of blocking the main thread on the request). */
static int g_srv_state;
static volatile int g_srv_fetching;
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" */
/* account switch (logout -> login): pause the sync thread while the main
* thread drops the old library and reloads, so it can't read/realloc the
* arrays mid-reset */
static volatile int g_pause_bg; /* request the sync thread to idle */
static volatile int g_sync_idle; /* sync thread acknowledges it idled */
/* ---- albums (read-only browsing) ------------------------------------ */
/* The album LIST lives in the g_album_* arrays, filled by GET /api/albums on
* a worker thread (the main thread only draws a throbber meanwhile). Each
* entry keeps the server's own ordering.
*
* Opening an album switches the grid to that album's assets, exactly like the
* smart-search result set: g_album_active marks the filter and g_album_idx
* holds indices into the g_asset_* arrays (which only ever grow). The members
* are fetched page by page with /api/search/metadata (withExif, so they carry
* date + aspect and drop straight into month sections).
*
* g_album_state: 0 not fetched yet, 1 ok, -1 failed. g_album_fetching: a
* worker thread is fetching the list right now. */
static char (*g_album_id)[40]; /* album uuid */
static char (*g_album_name)[64]; /* display name */
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 */
static int *g_album_idx; /* g_asset indices of its members */
static int g_album_idx_count;
static int g_album_idx_cap;
/* open-album fetch (worker thread): done flag, result, members so far */
static volatile int g_album_assets_done;
static volatile int g_album_assets_result;
static volatile int g_album_assets_loaded;
/* each album's cover as a g_asset index (asset_for_id), so the shared
* thumbnail worker loads it into g_thumb[] like any grid thumbnail.
* -1 = no cover (empty album) or not resolved yet. */
static int *g_album_cover;
/* 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
* evicted thumbnails crashes intermittently no matter how long the free is
* deferred. So thumbnails are never freed while browsing: released textures
* go into this pool and get reused for the next thumb with the same
* dimensions+format (Immich previews come in a handful of sizes, so the hit
* rate is high and the pool stays small). */
#define TEXPOOL_MAX 160
static vita2d_texture *g_texpool[TEXPOOL_MAX];
static int g_texpool_n;
static void tex_release(vita2d_texture *tex);
static vita2d_texture *tex_acquire(int w, int h, SceGxmTextureFormat fmt);
/* the sync thread asks the main thread to rebuild g_disp (which the main
* thread also reads every frame) by setting this; one writer per side */
static volatile int g_need_rebuild;
/* sync thread -> sync overview screen (loose: status text only) */
static volatile int g_sync_phase; /* 0 idle, 1 hashing, 2 checking, 3 uploading */
static char g_sync_activity[160];
static volatile curl_off_t g_ul_now, g_ul_total;
static char g_sync_errlog[5][160];
static volatile int g_sync_errn;
static void log_line(const char *fmt, ...);
/* grow *pp to hold `need` elements of `elem` bytes, doubling from `cur` cap;
* zero the freshly added tail (callers rely on NULL thumbs / zero state).
* returns the new capacity, or 0 on allocation failure. */
static int grow_array(void **pp, int cur, int need, size_t elem)
{
if (need <= cur)
return cur;
int cap = cur ? cur : 64;
while (cap < need)
cap *= 2;
void *np = realloc(*pp, (size_t)cap * elem);
if (!np)
return 0;
memset((char *)np + (size_t)cur * elem, 0, (size_t)(cap - cur) * elem);
*pp = np;
return cap;
}
#define GROW(arr, capvar, need) \
grow_array((void **)&(arr), (capvar), (need), sizeof(*(arr)))
/* ensure the server-asset arrays hold at least `need` entries */
static int grow_assets(int need)
{
int c = g_asset_cap;
if (need <= c)
return 1;
if (!GROW(g_asset_ids, c, need) ||
!GROW(g_asset_dates, c, need) ||
!GROW(g_asset_ratio, c, need) ||
!GROW(g_asset_rot, c, need) ||
!GROW(g_asset_is_video, c, need) ||
!GROW(g_asset_local_backed, c, need) ||
!GROW(g_asset_hidden, c, need) ||
!GROW(g_thumb, c, need) ||
!GROW(g_thumb_failed, c, need) ||
!(g_asset_cap = GROW(g_tex_err, c, need)))
return 0;
return 1;
}
/* ensure the local-media arrays hold at least `need` entries */
static int grow_locals(int need)
{
int c = g_local_cap;
if (need <= c)
return 1;
if (!GROW(g_local_path, c, need) ||
!GROW(g_local_date, c, need) ||
!GROW(g_local_sha1, c, need) ||
!GROW(g_local_server_id, c, need) ||
!GROW(g_local_err, c, need) ||
!GROW(g_local_mtime, c, need) ||
!GROW(g_local_ctime, c, need) ||
!GROW(g_local_size, c, need) ||
!GROW(g_local_is_video, c, need) ||
!GROW(g_local_state, c, need) ||
!GROW(g_local_thumb, c, need) ||
!(g_local_cap = GROW(g_local_thumb_failed, c, need)))
return 0;
return 1;
}
/* the merged timeline + its per-slot grid positions share one capacity */
static int grow_disp(int need)
{
int c = g_disp_cap;
if (need <= c)
return 1;
if (!GROW(g_disp, c, need) ||
!GROW(g_item_x, c, need) ||
!GROW(g_item_w, c, need) ||
!GROW(g_item_h, c, need) ||
!(g_disp_cap = GROW(g_item_y, c, need)))
return 0;
return 1;
}
static int grow_sect(int need)
{
if (need <= g_sect_cap)
return 1;
int cap = GROW(g_sect, g_sect_cap, need);
if (!cap)
return 0;
g_sect_cap = cap;
return 1;
}
static int grow_gallery(int need)
{
if (need <= g_gallery_cap)
return 1;
int cap = GROW(g_gallery_idx, g_gallery_cap, need);
if (!cap)
return 0;
g_gallery_cap = cap;
return 1;
}
/* ensure the album arrays hold at least `need` entries */
static int grow_albums(int need)
{
int c = g_album_cap;
if (need <= c)
return 1;
if (!GROW(g_album_id, c, need) ||
!GROW(g_album_name, c, need) ||
!GROW(g_album_thumb, c, 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;
}
/* ensure an open album's member index array holds at least `need` entries */
static int grow_album_idx(int need)
{
if (need <= g_album_idx_cap)
return 1;
int cap = GROW(g_album_idx, g_album_idx_cap, need);
if (!cap)
return 0;
g_album_idx_cap = cap;
return 1;
}
static void log_line(const char *fmt, ...)
{
FILE *f = fopen(LOG_PATH, "a");
if (!f)
return;
va_list ap;
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fputc('\n', f);
fclose(f);
}