Log libcurl's runtime version/SSL backend/protocol list at startup, mirror fatal errors into log.txt, and capture curl's verbose debug stream for the first request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1163 lines
34 KiB
C
1163 lines
34 KiB
C
/*
|
|
* vitaImmich — minimal proof-of-concept Immich client for the PS Vita.
|
|
*
|
|
* Reads server URL + API key from ux0:data/vitaimmich/config.txt and shows
|
|
* the photo library as a scrollable chronological grid (newest first).
|
|
* X opens a photo full screen, O goes back.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
#include <setjmp.h>
|
|
|
|
#include <psp2/ctrl.h>
|
|
#include <psp2/io/fcntl.h>
|
|
#include <psp2/io/stat.h>
|
|
#include <psp2/kernel/processmgr.h>
|
|
#include <psp2/kernel/threadmgr.h>
|
|
#include <psp2/net/net.h>
|
|
#include <psp2/net/netctl.h>
|
|
#include <psp2/sysmodule.h>
|
|
|
|
#include <vita2d.h>
|
|
#include <curl/curl.h>
|
|
#include <jpeglib.h>
|
|
|
|
#define JSMN_STATIC
|
|
#define JSMN_PARENT_LINKS
|
|
#include "jsmn.h"
|
|
|
|
#define SCREEN_W 960
|
|
#define SCREEN_H 544
|
|
|
|
#define COLS 4
|
|
#define CELL_W 240
|
|
#define CELL_H 240
|
|
#define CELL_PAD 6
|
|
#define THUMB_MAX 256 /* decode grid thumbs down to <= this dimension */
|
|
#define FULL_MAX 4096 /* GXM texture size limit */
|
|
|
|
#define PAGE_SIZE 100
|
|
#define MAX_ASSETS 1000
|
|
|
|
#define CONFIG_DIR "ux0:data/vitaimmich"
|
|
#define CONFIG_PATH CONFIG_DIR "/config.txt"
|
|
#define LOG_PATH CONFIG_DIR "/log.txt"
|
|
|
|
/* give curl/jpeg decoding plenty of heap */
|
|
int _newlib_heap_size_user = 192 * 1024 * 1024;
|
|
|
|
static char g_server[512];
|
|
static char g_apikey[256];
|
|
|
|
static vita2d_pgf *g_font;
|
|
|
|
static char g_asset_ids[MAX_ASSETS][40];
|
|
static char g_asset_dates[MAX_ASSETS][11]; /* YYYY-MM-DD */
|
|
static int g_asset_count;
|
|
static int g_next_page = 1; /* 0 = no more pages */
|
|
|
|
static vita2d_texture *g_thumb[MAX_ASSETS];
|
|
static int g_thumb_failed[MAX_ASSETS];
|
|
static char g_tex_err[MAX_ASSETS][160];
|
|
|
|
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);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* drawing helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void draw_centered(int y, uint32_t color, const char *text)
|
|
{
|
|
int w = vita2d_pgf_text_width(g_font, 1.0f, text);
|
|
vita2d_pgf_draw_text(g_font, (SCREEN_W - w) / 2, y, color, 1.0f, text);
|
|
}
|
|
|
|
static void show_status(const char *fmt, ...)
|
|
{
|
|
char buf[512];
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
va_end(ap);
|
|
|
|
vita2d_start_drawing();
|
|
vita2d_clear_screen();
|
|
draw_centered(SCREEN_H / 2, RGBA8(255, 255, 255, 255), buf);
|
|
vita2d_end_drawing();
|
|
vita2d_swap_buffers();
|
|
}
|
|
|
|
/* show an error (plus optional detail lines) until START is pressed, then exit */
|
|
static void fatal_error(const char *detail, const char *fmt, ...)
|
|
{
|
|
char buf[512];
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
va_end(ap);
|
|
|
|
log_line("FATAL: %s | %s", buf, detail ? detail : "-");
|
|
|
|
for (;;) {
|
|
SceCtrlData pad;
|
|
sceCtrlPeekBufferPositive(0, &pad, 1);
|
|
if (pad.buttons & SCE_CTRL_START)
|
|
break;
|
|
|
|
vita2d_start_drawing();
|
|
vita2d_clear_screen();
|
|
draw_centered(180, RGBA8(255, 80, 80, 255), buf);
|
|
if (detail && detail[0]) {
|
|
const int chars_per_line = 80;
|
|
int len = strlen(detail);
|
|
int y = 230;
|
|
for (int off = 0; off < len && y < 480; off += chars_per_line, y += 26) {
|
|
char line[96];
|
|
snprintf(line, sizeof(line), "%.*s", chars_per_line, detail + off);
|
|
vita2d_pgf_draw_text(g_font, 40, y, RGBA8(200, 200, 200, 255), 1.0f, line);
|
|
}
|
|
}
|
|
draw_centered(510, RGBA8(160, 160, 160, 255), "Press START to exit");
|
|
vita2d_end_drawing();
|
|
vita2d_swap_buffers();
|
|
}
|
|
|
|
vita2d_fini();
|
|
sceKernelExitProcess(0);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* config */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* trim whitespace on both ends and a UTF-8 BOM; editors sneak these in,
|
|
* and any of them makes curl fail with "unsupported protocol" */
|
|
static char *clean_line(char *s)
|
|
{
|
|
if (!strncmp(s, "\xEF\xBB\xBF", 3))
|
|
s += 3;
|
|
while (*s == ' ' || *s == '\t')
|
|
s++;
|
|
size_t n = strlen(s);
|
|
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r' ||
|
|
s[n - 1] == ' ' || s[n - 1] == '\t'))
|
|
s[--n] = '\0';
|
|
return s;
|
|
}
|
|
|
|
static int load_config(void)
|
|
{
|
|
FILE *f = fopen(CONFIG_PATH, "r");
|
|
if (!f) {
|
|
/* create a template so the user just has to edit it */
|
|
f = fopen(CONFIG_PATH, "w");
|
|
if (f) {
|
|
fputs("server=http://192.168.1.100:2283\n"
|
|
"apikey=PASTE_YOUR_IMMICH_API_KEY_HERE\n", f);
|
|
fclose(f);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
char line[512];
|
|
while (fgets(line, sizeof(line), f)) {
|
|
char *s = clean_line(line);
|
|
if (!strncmp(s, "server=", 7))
|
|
snprintf(g_server, sizeof(g_server), "%s", clean_line(s + 7));
|
|
else if (!strncmp(s, "apikey=", 7))
|
|
snprintf(g_apikey, sizeof(g_apikey), "%s", clean_line(s + 7));
|
|
}
|
|
fclose(f);
|
|
|
|
/* strip trailing slash from server URL */
|
|
size_t n = strlen(g_server);
|
|
if (n > 0 && g_server[n - 1] == '/')
|
|
g_server[n - 1] = '\0';
|
|
|
|
if (!g_server[0] || !g_apikey[0] || strstr(g_apikey, "PASTE_YOUR"))
|
|
return -1;
|
|
|
|
/* default to http:// when no scheme is given */
|
|
if (!strstr(g_server, "://")) {
|
|
char tmp[512];
|
|
snprintf(tmp, sizeof(tmp), "http://%s", g_server);
|
|
snprintf(g_server, sizeof(g_server), "%s", tmp);
|
|
}
|
|
if (strncasecmp(g_server, "http://", 7) && strncasecmp(g_server, "https://", 8))
|
|
fatal_error(g_server,
|
|
"Bad server URL in config (must start with http:// or https://):");
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* networking */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static char g_net_mem[1024 * 1024];
|
|
|
|
static void net_init(void)
|
|
{
|
|
sceSysmoduleLoadModule(SCE_SYSMODULE_NET);
|
|
if (sceNetShowNetstat() == SCE_NET_ERROR_ENOTINIT) {
|
|
SceNetInitParam param = {
|
|
.memory = g_net_mem,
|
|
.size = sizeof(g_net_mem),
|
|
.flags = 0,
|
|
};
|
|
sceNetInit(¶m);
|
|
}
|
|
sceNetCtlInit();
|
|
curl_global_init(CURL_GLOBAL_ALL);
|
|
}
|
|
|
|
typedef struct {
|
|
char *data;
|
|
size_t size;
|
|
} membuf;
|
|
|
|
static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata)
|
|
{
|
|
membuf *mb = userdata;
|
|
size_t add = size * nmemb;
|
|
char *p = realloc(mb->data, mb->size + add + 1);
|
|
if (!p)
|
|
return 0;
|
|
mb->data = p;
|
|
memcpy(mb->data + mb->size, ptr, add);
|
|
mb->size += add;
|
|
mb->data[mb->size] = '\0';
|
|
return add;
|
|
}
|
|
|
|
/* verbose-log the first request via curl's debug stream so connection
|
|
* problems land in log.txt with curl's own explanation */
|
|
static int g_verbose_request = 1;
|
|
|
|
static int curl_debug_cb(CURL *h, curl_infotype type, char *data,
|
|
size_t size, void *ud)
|
|
{
|
|
if (type == CURLINFO_TEXT && size > 0) {
|
|
char line[300];
|
|
if (data[size - 1] == '\n')
|
|
size--;
|
|
snprintf(line, sizeof(line), "%.*s", (int)size, data);
|
|
log_line("curl: %s", line);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* body == NULL -> GET, otherwise POST with a JSON body.
|
|
* ctype (optional) receives the response Content-Type. */
|
|
static CURLcode http_request(const char *url, const char *body,
|
|
membuf *out, long *http_code,
|
|
char *ctype, size_t ctype_len)
|
|
{
|
|
out->data = NULL;
|
|
out->size = 0;
|
|
*http_code = 0;
|
|
if (ctype)
|
|
ctype[0] = '\0';
|
|
|
|
CURL *curl = curl_easy_init();
|
|
if (!curl)
|
|
return CURLE_FAILED_INIT;
|
|
|
|
char keyhdr[300];
|
|
snprintf(keyhdr, sizeof(keyhdr), "x-api-key: %s", g_apikey);
|
|
|
|
struct curl_slist *hdrs = NULL;
|
|
hdrs = curl_slist_append(hdrs, keyhdr);
|
|
hdrs = curl_slist_append(hdrs, "Accept: application/json, image/jpeg");
|
|
if (body)
|
|
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
|
|
|
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, out);
|
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "vitaImmich/0.1 (PS Vita)");
|
|
/* PoC: no CA bundle on the Vita, skip TLS verification */
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
|
if (body)
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
|
if (g_verbose_request) {
|
|
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_debug_cb);
|
|
}
|
|
|
|
CURLcode res = curl_easy_perform(curl);
|
|
if (g_verbose_request) {
|
|
log_line("curl: perform result=%d (%s)", res, curl_easy_strerror(res));
|
|
g_verbose_request = 0;
|
|
}
|
|
if (res == CURLE_OK) {
|
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code);
|
|
if (ctype) {
|
|
char *ct = NULL;
|
|
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct);
|
|
if (ct)
|
|
snprintf(ctype, ctype_len, "%s", ct);
|
|
}
|
|
}
|
|
|
|
curl_slist_free_all(hdrs);
|
|
curl_easy_cleanup(curl);
|
|
return res;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* JPEG decoding */
|
|
/* */
|
|
/* libjpeg directly instead of vita2d_load_JPEG_buffer: vita2d rejects */
|
|
/* any JPEG whose first marker isn't APP0/APP1, but Immich previews */
|
|
/* start with an APP2 ICC-profile marker (ff d8 ff e2). Also lets us */
|
|
/* downscale at decode time for grid thumbnails, and a longjmp error */
|
|
/* handler keeps corrupt data from exit()ing the app. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
struct jpeg_jmp_err {
|
|
struct jpeg_error_mgr mgr;
|
|
jmp_buf jb;
|
|
char msg[JMSG_LENGTH_MAX];
|
|
};
|
|
|
|
static void jpeg_jmp_error_exit(j_common_ptr cinfo)
|
|
{
|
|
struct jpeg_jmp_err *e = (struct jpeg_jmp_err *)cinfo->err;
|
|
cinfo->err->format_message(cinfo, e->msg);
|
|
longjmp(e->jb, 1);
|
|
}
|
|
|
|
static vita2d_texture *decode_jpeg(const void *data, size_t size, int maxdim,
|
|
char *err, size_t errlen)
|
|
{
|
|
struct jpeg_decompress_struct ji;
|
|
struct jpeg_jmp_err jerr;
|
|
vita2d_texture *tex = NULL;
|
|
|
|
ji.err = jpeg_std_error(&jerr.mgr);
|
|
jerr.mgr.error_exit = jpeg_jmp_error_exit;
|
|
if (setjmp(jerr.jb)) {
|
|
snprintf(err, errlen, "libjpeg: %s", jerr.msg);
|
|
jpeg_destroy_decompress(&ji);
|
|
if (tex)
|
|
vita2d_free_texture(tex);
|
|
return NULL;
|
|
}
|
|
|
|
jpeg_create_decompress(&ji);
|
|
jpeg_mem_src(&ji, (void *)data, size);
|
|
jpeg_read_header(&ji, 1);
|
|
|
|
unsigned int longer = ji.image_width > ji.image_height ?
|
|
ji.image_width : ji.image_height;
|
|
ji.scale_num = 1;
|
|
ji.scale_denom = 1;
|
|
while (longer / ji.scale_denom > (unsigned int)maxdim && ji.scale_denom < 8)
|
|
ji.scale_denom *= 2;
|
|
|
|
jpeg_start_decompress(&ji);
|
|
|
|
if (ji.output_components != 1 && ji.output_components != 3) {
|
|
snprintf(err, errlen, "unsupported JPEG: %d components (colorspace %d)",
|
|
ji.output_components, ji.jpeg_color_space);
|
|
jpeg_abort_decompress(&ji);
|
|
jpeg_destroy_decompress(&ji);
|
|
return NULL;
|
|
}
|
|
|
|
SceGxmTextureFormat fmt = (ji.output_components == 1) ?
|
|
SCE_GXM_TEXTURE_FORMAT_U8_R111 : SCE_GXM_TEXTURE_FORMAT_U8U8U8_BGR;
|
|
tex = vita2d_create_empty_texture_format(ji.output_width,
|
|
ji.output_height, fmt);
|
|
if (!tex) {
|
|
snprintf(err, errlen, "texture alloc failed (%ux%u)",
|
|
ji.output_width, ji.output_height);
|
|
jpeg_abort_decompress(&ji);
|
|
jpeg_destroy_decompress(&ji);
|
|
return NULL;
|
|
}
|
|
|
|
unsigned char *row = vita2d_texture_get_datap(tex);
|
|
unsigned int stride = vita2d_texture_get_stride(tex);
|
|
while (ji.output_scanline < ji.output_height) {
|
|
jpeg_read_scanlines(&ji, &row, 1);
|
|
row += stride;
|
|
}
|
|
|
|
jpeg_finish_decompress(&ji);
|
|
jpeg_destroy_decompress(&ji);
|
|
return tex;
|
|
}
|
|
|
|
/* decode into a tightly packed malloc'd pixel buffer; no vita2d/GXM
|
|
* calls, so this is safe to run on the loader thread */
|
|
static unsigned char *decode_jpeg_buf(const void *data, size_t size, int maxdim,
|
|
int *w, int *h, int *comps,
|
|
char *err, size_t errlen)
|
|
{
|
|
struct jpeg_decompress_struct ji;
|
|
struct jpeg_jmp_err jerr;
|
|
unsigned char *pix = NULL;
|
|
|
|
ji.err = jpeg_std_error(&jerr.mgr);
|
|
jerr.mgr.error_exit = jpeg_jmp_error_exit;
|
|
if (setjmp(jerr.jb)) {
|
|
snprintf(err, errlen, "libjpeg: %s", jerr.msg);
|
|
jpeg_destroy_decompress(&ji);
|
|
free(pix);
|
|
return NULL;
|
|
}
|
|
|
|
jpeg_create_decompress(&ji);
|
|
jpeg_mem_src(&ji, (void *)data, size);
|
|
jpeg_read_header(&ji, 1);
|
|
|
|
unsigned int longer = ji.image_width > ji.image_height ?
|
|
ji.image_width : ji.image_height;
|
|
ji.scale_num = 1;
|
|
ji.scale_denom = 1;
|
|
while (longer / ji.scale_denom > (unsigned int)maxdim && ji.scale_denom < 8)
|
|
ji.scale_denom *= 2;
|
|
|
|
jpeg_start_decompress(&ji);
|
|
|
|
if (ji.output_components != 1 && ji.output_components != 3) {
|
|
snprintf(err, errlen, "unsupported JPEG: %d components (colorspace %d)",
|
|
ji.output_components, ji.jpeg_color_space);
|
|
jpeg_abort_decompress(&ji);
|
|
jpeg_destroy_decompress(&ji);
|
|
return NULL;
|
|
}
|
|
|
|
unsigned int rowbytes = ji.output_width * ji.output_components;
|
|
pix = malloc(rowbytes * ji.output_height);
|
|
if (!pix) {
|
|
snprintf(err, errlen, "out of memory (%ux%u)",
|
|
ji.output_width, ji.output_height);
|
|
jpeg_abort_decompress(&ji);
|
|
jpeg_destroy_decompress(&ji);
|
|
return NULL;
|
|
}
|
|
|
|
unsigned char *row = pix;
|
|
while (ji.output_scanline < ji.output_height) {
|
|
jpeg_read_scanlines(&ji, &row, 1);
|
|
row += rowbytes;
|
|
}
|
|
|
|
*w = ji.output_width;
|
|
*h = ji.output_height;
|
|
*comps = ji.output_components;
|
|
jpeg_finish_decompress(&ji);
|
|
jpeg_destroy_decompress(&ji);
|
|
return pix;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* immich API */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* append assets from a search/metadata (or search/random) response */
|
|
static int parse_assets(const char *js, size_t jslen, char *err, size_t errlen)
|
|
{
|
|
jsmn_parser parser;
|
|
jsmn_init(&parser);
|
|
int ntok = jsmn_parse(&parser, js, jslen, NULL, 0);
|
|
if (ntok <= 0) {
|
|
snprintf(err, errlen, "JSON parse error (%d)", ntok);
|
|
return -1;
|
|
}
|
|
|
|
jsmntok_t *tok = malloc(sizeof(jsmntok_t) * ntok);
|
|
if (!tok) {
|
|
snprintf(err, errlen, "out of memory (%d tokens)", ntok);
|
|
return -1;
|
|
}
|
|
jsmn_init(&parser);
|
|
ntok = jsmn_parse(&parser, js, jslen, tok, ntok);
|
|
if (ntok <= 0) {
|
|
free(tok);
|
|
snprintf(err, errlen, "JSON parse error (%d)", ntok);
|
|
return -1;
|
|
}
|
|
|
|
/* locate the asset array: root itself, or assets.items */
|
|
int arr_idx = -1;
|
|
if (tok[0].type == JSMN_ARRAY) {
|
|
arr_idx = 0;
|
|
} else {
|
|
int assets_obj = -1;
|
|
for (int i = 1; i < ntok - 1; i++)
|
|
if (tok[i].type == JSMN_STRING && tok[i].size == 1 &&
|
|
tok[i].parent == 0 &&
|
|
tok[i].end - tok[i].start == 6 &&
|
|
!strncmp(js + tok[i].start, "assets", 6) &&
|
|
tok[i + 1].type == JSMN_OBJECT) {
|
|
assets_obj = i + 1;
|
|
break;
|
|
}
|
|
for (int i = assets_obj + 1; assets_obj > 0 && i < ntok - 1; i++)
|
|
if (tok[i].type == JSMN_STRING && tok[i].size == 1 &&
|
|
tok[i].parent == assets_obj &&
|
|
tok[i].end - tok[i].start == 5 &&
|
|
!strncmp(js + tok[i].start, "items", 5) &&
|
|
tok[i + 1].type == JSMN_ARRAY) {
|
|
arr_idx = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
if (arr_idx < 0) {
|
|
free(tok);
|
|
snprintf(err, errlen, "unexpected response shape");
|
|
return -1;
|
|
}
|
|
|
|
/* walk tokens in document order; keys whose parent is the current
|
|
* top-level object belong to that asset (nested objects have a
|
|
* different parent, so owner.id etc. are skipped) */
|
|
int added = 0;
|
|
int cur = -1, cur_obj = -1;
|
|
for (int i = arr_idx + 1; i < ntok; i++) {
|
|
if (tok[i].type == JSMN_OBJECT && tok[i].parent == arr_idx) {
|
|
if (g_asset_count >= MAX_ASSETS)
|
|
break;
|
|
cur = g_asset_count++;
|
|
cur_obj = i;
|
|
added++;
|
|
g_asset_ids[cur][0] = '\0';
|
|
g_asset_dates[cur][0] = '\0';
|
|
continue;
|
|
}
|
|
if (cur < 0 || tok[i].type != JSMN_STRING || tok[i].size != 1 ||
|
|
tok[i].parent != cur_obj || i + 1 >= ntok)
|
|
continue;
|
|
|
|
int klen = tok[i].end - tok[i].start;
|
|
const char *k = js + tok[i].start;
|
|
jsmntok_t *val = &tok[i + 1];
|
|
int vlen = val->end - val->start;
|
|
if (val->type != JSMN_STRING)
|
|
continue;
|
|
|
|
if (klen == 2 && !strncmp(k, "id", 2) &&
|
|
vlen > 0 && vlen < (int)sizeof(g_asset_ids[0]))
|
|
snprintf(g_asset_ids[cur], sizeof(g_asset_ids[0]),
|
|
"%.*s", vlen, js + val->start);
|
|
else if (klen == 13 && !strncmp(k, "fileCreatedAt", 13) && vlen >= 10)
|
|
snprintf(g_asset_dates[cur], sizeof(g_asset_dates[0]),
|
|
"%.10s", js + val->start);
|
|
}
|
|
free(tok);
|
|
|
|
/* drop trailing entries that somehow lack an id */
|
|
while (g_asset_count > 0 && g_asset_ids[g_asset_count - 1][0] == '\0') {
|
|
g_asset_count--;
|
|
added--;
|
|
}
|
|
return added;
|
|
}
|
|
|
|
/* fetch the next page of the library, newest first; returns assets added */
|
|
static int fetch_page(int is_first)
|
|
{
|
|
if (g_next_page <= 0 || g_asset_count >= MAX_ASSETS)
|
|
return 0;
|
|
|
|
char url[600];
|
|
snprintf(url, sizeof(url), "%s/api/search/metadata", g_server);
|
|
char body[128];
|
|
snprintf(body, sizeof(body),
|
|
"{\"page\":%d,\"size\":%d,\"type\":\"IMAGE\",\"order\":\"desc\"}",
|
|
g_next_page, PAGE_SIZE);
|
|
|
|
membuf buf;
|
|
long code;
|
|
CURLcode res = http_request(url, body, &buf, &code, NULL, 0);
|
|
if (res != CURLE_OK) {
|
|
if (is_first) {
|
|
char det[700];
|
|
snprintf(det, sizeof(det), "%s -- URL: %s",
|
|
curl_easy_strerror(res), url);
|
|
fatal_error(det, "Connection failed (curl error %d)", res);
|
|
}
|
|
log_line("page %d: curl error %d", g_next_page, res);
|
|
g_next_page = 0;
|
|
free(buf.data);
|
|
return 0;
|
|
}
|
|
if (code < 200 || code >= 300) {
|
|
if (is_first)
|
|
fatal_error(buf.data, "Server returned HTTP %ld", code);
|
|
log_line("page %d: HTTP %ld: %.200s", g_next_page, code,
|
|
buf.data ? buf.data : "");
|
|
g_next_page = 0;
|
|
free(buf.data);
|
|
return 0;
|
|
}
|
|
|
|
char err[160];
|
|
int added = parse_assets(buf.data, buf.size, err, sizeof(err));
|
|
if (added < 0) {
|
|
if (is_first)
|
|
fatal_error(buf.data, "Could not parse server response: %s", err);
|
|
log_line("page %d: %s", g_next_page, err);
|
|
g_next_page = 0;
|
|
free(buf.data);
|
|
return 0;
|
|
}
|
|
free(buf.data);
|
|
|
|
/* a short page means we reached the end of the library */
|
|
g_next_page = (added == PAGE_SIZE) ? g_next_page + 1 : 0;
|
|
log_line("page fetched: +%d assets (total %d)", added, g_asset_count);
|
|
return added;
|
|
}
|
|
|
|
/* download the preview JPEG of an asset and decode it to <= maxdim px */
|
|
static vita2d_texture *load_image(int idx, int maxdim)
|
|
{
|
|
char *err = g_tex_err[idx];
|
|
const size_t errlen = sizeof(g_tex_err[idx]);
|
|
|
|
char url[700];
|
|
snprintf(url, sizeof(url), "%s/api/assets/%s/thumbnail?size=preview",
|
|
g_server, g_asset_ids[idx]);
|
|
|
|
membuf buf;
|
|
long code;
|
|
char ctype[80];
|
|
CURLcode res = http_request(url, NULL, &buf, &code, ctype, sizeof(ctype));
|
|
|
|
if (res != CURLE_OK) {
|
|
snprintf(err, errlen, "curl error %d: %s", res, curl_easy_strerror(res));
|
|
log_line("%s: %s", g_asset_ids[idx], err);
|
|
free(buf.data);
|
|
return NULL;
|
|
}
|
|
if (code < 200 || code >= 300) {
|
|
snprintf(err, errlen, "HTTP %ld: %.100s", code,
|
|
buf.data ? buf.data : "(empty body)");
|
|
log_line("%s: %s", g_asset_ids[idx], err);
|
|
free(buf.data);
|
|
return NULL;
|
|
}
|
|
if (buf.size == 0) {
|
|
snprintf(err, errlen, "HTTP %ld but empty body (type %s)", code, ctype);
|
|
log_line("%s: %s", g_asset_ids[idx], err);
|
|
free(buf.data);
|
|
return NULL;
|
|
}
|
|
|
|
vita2d_texture *tex = NULL;
|
|
const unsigned char *p = (const unsigned char *)buf.data;
|
|
if (buf.size > 2 && p[0] == 0xff && p[1] == 0xd8) {
|
|
tex = decode_jpeg(buf.data, buf.size, maxdim, err, errlen);
|
|
} else if (buf.size > 8 && !memcmp(p, "\x89PNG", 4)) {
|
|
tex = vita2d_load_PNG_buffer(buf.data);
|
|
if (!tex)
|
|
snprintf(err, errlen, "PNG decode failed; %u bytes",
|
|
(unsigned)buf.size);
|
|
} else if (buf.size > 12 && !memcmp(p, "RIFF", 4) && !memcmp(p + 8, "WEBP", 4)) {
|
|
snprintf(err, errlen, "got WebP (unsupported); %u bytes, type %s",
|
|
(unsigned)buf.size, ctype);
|
|
} else {
|
|
snprintf(err, errlen,
|
|
"unknown format; %u bytes, type %s, magic %02x %02x %02x %02x",
|
|
(unsigned)buf.size, ctype, p[0], p[1],
|
|
buf.size > 2 ? p[2] : 0, buf.size > 3 ? p[3] : 0);
|
|
}
|
|
if (!tex)
|
|
log_line("%s: %s", g_asset_ids[idx], err);
|
|
free(buf.data);
|
|
return tex;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* background thumbnail loader */
|
|
/* */
|
|
/* One worker thread with a single request slot. The worker downloads */
|
|
/* (reusing one curl handle, so the HTTP connection stays alive) and */
|
|
/* decodes JPEGs to a plain pixel buffer; the main thread turns the */
|
|
/* result into a GXM texture. Handoff is a tiny state machine: */
|
|
/* IDLE -> (main writes request) PENDING -> (worker writes result) */
|
|
/* DONE -> (main consumes) IDLE. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
enum { REQ_IDLE, REQ_PENDING, REQ_DONE };
|
|
|
|
static volatile int g_req_state = REQ_IDLE;
|
|
static volatile int g_req_idx = -1;
|
|
static unsigned char *g_req_pix; /* decoded pixels, or NULL on failure */
|
|
static int g_req_w, g_req_h, g_req_comps;
|
|
static char *g_req_raw; /* raw body when it's a PNG */
|
|
static size_t g_req_raw_size;
|
|
|
|
static int worker_thread(SceSize args, void *argp)
|
|
{
|
|
CURL *curl = curl_easy_init();
|
|
char keyhdr[300];
|
|
snprintf(keyhdr, sizeof(keyhdr), "x-api-key: %s", g_apikey);
|
|
struct curl_slist *hdrs = curl_slist_append(NULL, keyhdr);
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "vitaImmich/0.1 (PS Vita)");
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
|
|
|
for (;;) {
|
|
if (g_req_state != REQ_PENDING) {
|
|
sceKernelDelayThread(2000);
|
|
continue;
|
|
}
|
|
int idx = g_req_idx;
|
|
char *err = g_tex_err[idx];
|
|
const size_t errlen = sizeof(g_tex_err[idx]);
|
|
g_req_pix = NULL;
|
|
g_req_raw = NULL;
|
|
|
|
char url[700];
|
|
snprintf(url, sizeof(url), "%s/api/assets/%s/thumbnail?size=preview",
|
|
g_server, g_asset_ids[idx]);
|
|
|
|
membuf buf = { NULL, 0 };
|
|
long code = 0;
|
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
|
|
CURLcode res = curl_easy_perform(curl);
|
|
if (res == CURLE_OK)
|
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
|
|
|
|
if (res != CURLE_OK) {
|
|
snprintf(err, errlen, "curl error %d: %s",
|
|
res, curl_easy_strerror(res));
|
|
} else if (code < 200 || code >= 300) {
|
|
snprintf(err, errlen, "HTTP %ld: %.100s", code,
|
|
buf.data ? buf.data : "(empty body)");
|
|
} else if (buf.size == 0) {
|
|
snprintf(err, errlen, "HTTP %ld but empty body", code);
|
|
} else {
|
|
const unsigned char *p = (const unsigned char *)buf.data;
|
|
if (buf.size > 2 && p[0] == 0xff && p[1] == 0xd8) {
|
|
g_req_pix = decode_jpeg_buf(buf.data, buf.size,
|
|
THUMB_MAX,
|
|
&g_req_w, &g_req_h,
|
|
&g_req_comps,
|
|
err, errlen);
|
|
} else if (buf.size > 8 && !memcmp(p, "\x89PNG", 4)) {
|
|
/* hand the raw bytes to the main thread,
|
|
* vita2d's PNG loader isn't used off-thread */
|
|
g_req_raw = buf.data;
|
|
g_req_raw_size = buf.size;
|
|
buf.data = NULL;
|
|
} else if (buf.size > 12 && !memcmp(p, "RIFF", 4) &&
|
|
!memcmp(p + 8, "WEBP", 4)) {
|
|
snprintf(err, errlen, "got WebP (unsupported); %u bytes",
|
|
(unsigned)buf.size);
|
|
} else {
|
|
snprintf(err, errlen,
|
|
"unknown format; %u bytes, magic %02x %02x %02x %02x",
|
|
(unsigned)buf.size, p[0], p[1],
|
|
buf.size > 2 ? p[2] : 0,
|
|
buf.size > 3 ? p[3] : 0);
|
|
}
|
|
}
|
|
free(buf.data);
|
|
if (!g_req_pix && !g_req_raw)
|
|
log_line("%s: %s", g_asset_ids[idx], err);
|
|
|
|
__sync_synchronize();
|
|
g_req_state = REQ_DONE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* main-thread side: turn a finished worker result into a texture */
|
|
static void consume_worker_result(void)
|
|
{
|
|
int idx = g_req_idx;
|
|
vita2d_texture *tex = NULL;
|
|
|
|
if (g_req_pix) {
|
|
SceGxmTextureFormat fmt = (g_req_comps == 1) ?
|
|
SCE_GXM_TEXTURE_FORMAT_U8_R111 :
|
|
SCE_GXM_TEXTURE_FORMAT_U8U8U8_BGR;
|
|
tex = vita2d_create_empty_texture_format(g_req_w, g_req_h, fmt);
|
|
if (tex) {
|
|
unsigned char *dst = vita2d_texture_get_datap(tex);
|
|
unsigned int stride = vita2d_texture_get_stride(tex);
|
|
unsigned int rowbytes = g_req_w * g_req_comps;
|
|
for (int y = 0; y < g_req_h; y++)
|
|
memcpy(dst + y * stride,
|
|
g_req_pix + y * rowbytes, rowbytes);
|
|
} else {
|
|
snprintf(g_tex_err[idx], sizeof(g_tex_err[idx]),
|
|
"texture alloc failed (%dx%d)", g_req_w, g_req_h);
|
|
}
|
|
free(g_req_pix);
|
|
g_req_pix = NULL;
|
|
} else if (g_req_raw) {
|
|
tex = vita2d_load_PNG_buffer(g_req_raw);
|
|
if (!tex)
|
|
snprintf(g_tex_err[idx], sizeof(g_tex_err[idx]),
|
|
"PNG decode failed; %u bytes",
|
|
(unsigned)g_req_raw_size);
|
|
free(g_req_raw);
|
|
g_req_raw = NULL;
|
|
}
|
|
|
|
g_thumb[idx] = tex;
|
|
g_thumb_failed[idx] = (tex == NULL);
|
|
g_req_state = REQ_IDLE;
|
|
}
|
|
|
|
/* pick the most useful thumbnail to load next: selection, then visible,
|
|
* then prefetch a couple of rows below and above the viewport */
|
|
static int pick_next_load(int sel, int first_vis, int last_vis)
|
|
{
|
|
if (!g_thumb[sel] && !g_thumb_failed[sel])
|
|
return sel;
|
|
for (int i = first_vis; i >= 0 && i <= last_vis && i < g_asset_count; i++)
|
|
if (!g_thumb[i] && !g_thumb_failed[i])
|
|
return i;
|
|
for (int i = last_vis + 1; i <= last_vis + 2 * COLS && i < g_asset_count; i++)
|
|
if (i >= 0 && !g_thumb[i] && !g_thumb_failed[i])
|
|
return i;
|
|
for (int i = first_vis - 1; i >= first_vis - 2 * COLS && i >= 0; i--)
|
|
if (!g_thumb[i] && !g_thumb_failed[i])
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* UI */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void draw_texture_fitted(vita2d_texture *tex, float x, float y,
|
|
float box_w, float box_h)
|
|
{
|
|
float w = vita2d_texture_get_width(tex);
|
|
float h = vita2d_texture_get_height(tex);
|
|
float s = (box_w / w < box_h / h) ? box_w / w : box_h / h;
|
|
vita2d_draw_texture_scale(tex, x + (box_w - w * s) / 2.0f,
|
|
y + (box_h - h * s) / 2.0f, s, s);
|
|
}
|
|
|
|
static void draw_sel_outline(float x, float y, float w, float h)
|
|
{
|
|
const uint32_t c = RGBA8(255, 255, 255, 255);
|
|
const float t = 3.0f;
|
|
vita2d_draw_rectangle(x, y, w, t, c);
|
|
vita2d_draw_rectangle(x, y + h - t, w, t, c);
|
|
vita2d_draw_rectangle(x, y, t, h, c);
|
|
vita2d_draw_rectangle(x + w - t, y, t, h, c);
|
|
}
|
|
|
|
static void draw_hud(const char *text)
|
|
{
|
|
vita2d_draw_rectangle(0, SCREEN_H - 32, SCREEN_W, 32, RGBA8(0, 0, 0, 180));
|
|
vita2d_pgf_draw_text(g_font, 10, SCREEN_H - 9,
|
|
RGBA8(255, 255, 255, 255), 1.0f, text);
|
|
}
|
|
|
|
static void draw_error_detail(int idx)
|
|
{
|
|
draw_centered(220, RGBA8(255, 80, 80, 255), "Failed to load this photo");
|
|
vita2d_pgf_draw_text(g_font, 40, 270, RGBA8(200, 200, 200, 255),
|
|
1.0f, g_asset_ids[idx]);
|
|
const char *e = g_tex_err[idx];
|
|
int len = strlen(e), y = 300;
|
|
const int per_line = 76;
|
|
for (int off = 0; off < len && y < 500; off += per_line, y += 26) {
|
|
char line[80];
|
|
snprintf(line, sizeof(line), "%.*s", per_line, e + off);
|
|
vita2d_pgf_draw_text(g_font, 40, y, RGBA8(200, 200, 200, 255), 1.0f, line);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* main */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
enum { MODE_GRID, MODE_DETAIL };
|
|
|
|
int main(void)
|
|
{
|
|
vita2d_init();
|
|
vita2d_set_clear_color(RGBA8(16, 16, 16, 255));
|
|
g_font = vita2d_load_default_pgf();
|
|
|
|
/* start a fresh debug log each launch */
|
|
sceIoMkdir(CONFIG_DIR, 0777);
|
|
FILE *lf = fopen(LOG_PATH, "w");
|
|
if (lf)
|
|
fclose(lf);
|
|
|
|
show_status("Starting network...");
|
|
net_init();
|
|
|
|
/* runtime truth about what the linked libcurl supports */
|
|
{
|
|
curl_version_info_data *vi = curl_version_info(CURLVERSION_NOW);
|
|
log_line("libcurl %s | ssl: %s", vi->version,
|
|
vi->ssl_version ? vi->ssl_version : "(none)");
|
|
char plist[300] = "";
|
|
for (const char *const *pp = vi->protocols; *pp; pp++) {
|
|
strncat(plist, *pp, sizeof(plist) - strlen(plist) - 2);
|
|
strncat(plist, " ", sizeof(plist) - strlen(plist) - 1);
|
|
}
|
|
log_line("protocols: %s", plist);
|
|
}
|
|
|
|
if (load_config() != 0)
|
|
fatal_error("Edit it with VitaShell, then restart the app. "
|
|
"Create the API key in the Immich web UI under "
|
|
"Account Settings > API Keys.",
|
|
"Set your server and API key in " CONFIG_PATH);
|
|
|
|
/* log the exact bytes of the server URL; invisible characters in the
|
|
* config show up here when curl complains about the protocol */
|
|
{
|
|
char hex[3 * 64 + 1] = "";
|
|
int n = strlen(g_server);
|
|
for (int i = 0; i < n && i < 64; i++)
|
|
sprintf(hex + 3 * i, "%02x ", (unsigned char)g_server[i]);
|
|
log_line("server='%s' len=%d hex=%s", g_server, n, hex);
|
|
}
|
|
|
|
show_status("Loading library from %s ...", g_server);
|
|
fetch_page(1);
|
|
if (g_asset_count == 0)
|
|
fatal_error(NULL, "Server returned no image assets");
|
|
|
|
SceUID worker = sceKernelCreateThread("thumb_loader", worker_thread,
|
|
0x10000100, 256 * 1024, 0, 0, NULL);
|
|
if (worker >= 0)
|
|
sceKernelStartThread(worker, 0, NULL);
|
|
|
|
int mode = MODE_GRID;
|
|
int sel = 0;
|
|
float scroll = 0.0f, target = 0.0f;
|
|
unsigned int prev_buttons = 0;
|
|
unsigned int held_frames = 0;
|
|
|
|
vita2d_texture *detail_tex = NULL;
|
|
int detail_idx = -1;
|
|
int detail_failed = 0;
|
|
|
|
for (;;) {
|
|
SceCtrlData pad;
|
|
sceCtrlPeekBufferPositive(0, &pad, 1);
|
|
unsigned int pressed = pad.buttons & ~prev_buttons;
|
|
prev_buttons = pad.buttons;
|
|
|
|
if (pad.buttons & SCE_CTRL_START)
|
|
break;
|
|
|
|
/* d-pad auto-repeat for fast scrolling */
|
|
const unsigned int dirs = SCE_CTRL_UP | SCE_CTRL_DOWN |
|
|
SCE_CTRL_LEFT | SCE_CTRL_RIGHT;
|
|
held_frames = (pad.buttons & dirs) ? held_frames + 1 : 0;
|
|
unsigned int nav = pressed;
|
|
if (held_frames > 18 && held_frames % 5 == 0)
|
|
nav |= pad.buttons & dirs;
|
|
|
|
if (mode == MODE_GRID) {
|
|
if (nav & SCE_CTRL_RIGHT)
|
|
sel++;
|
|
if (nav & SCE_CTRL_LEFT)
|
|
sel--;
|
|
if (nav & SCE_CTRL_DOWN)
|
|
sel += COLS;
|
|
if (nav & SCE_CTRL_UP)
|
|
sel -= COLS;
|
|
if (pressed & SCE_CTRL_RTRIGGER)
|
|
sel += COLS * 2; /* page down */
|
|
if (pressed & SCE_CTRL_LTRIGGER)
|
|
sel -= COLS * 2;
|
|
if (sel < 0)
|
|
sel = 0;
|
|
if (sel >= g_asset_count)
|
|
sel = g_asset_count - 1;
|
|
|
|
if (pressed & SCE_CTRL_CROSS)
|
|
mode = MODE_DETAIL;
|
|
|
|
/* fetch the next page when selection nears the end */
|
|
if (g_next_page > 0 &&
|
|
sel >= g_asset_count - COLS * 4) {
|
|
show_status("Loading more photos... (%d so far)",
|
|
g_asset_count);
|
|
fetch_page(0);
|
|
}
|
|
|
|
/* scroll follows the selection */
|
|
float sel_y = (float)(sel / COLS) * CELL_H;
|
|
if (sel_y < target)
|
|
target = sel_y;
|
|
if (sel_y + CELL_H > target + SCREEN_H)
|
|
target = sel_y + CELL_H - SCREEN_H;
|
|
int rows = (g_asset_count + COLS - 1) / COLS;
|
|
float max_scroll = rows * CELL_H - SCREEN_H;
|
|
if (max_scroll < 0)
|
|
max_scroll = 0;
|
|
if (target < 0)
|
|
target = 0;
|
|
if (target > max_scroll)
|
|
target = max_scroll;
|
|
scroll += (target - scroll) * 0.35f;
|
|
|
|
int first_vis = ((int)scroll / CELL_H) * COLS;
|
|
int last_vis = (((int)scroll + SCREEN_H) / CELL_H + 1) * COLS - 1;
|
|
if (last_vis >= g_asset_count)
|
|
last_vis = g_asset_count - 1;
|
|
|
|
/* evict thumbs far outside the viewport (never the one
|
|
* the worker is currently loading); wait for the GPU
|
|
* before the first free in case a recently drawn
|
|
* texture is still referenced by an in-flight frame */
|
|
int waited = 0;
|
|
for (int i = 0; i < g_asset_count; i++) {
|
|
if (!g_thumb[i] ||
|
|
(g_req_state != REQ_IDLE && i == g_req_idx))
|
|
continue;
|
|
float dy = (float)(i / COLS) * CELL_H - scroll;
|
|
if (dy < -2.5f * SCREEN_H || dy > 3.5f * SCREEN_H) {
|
|
if (!waited) {
|
|
vita2d_wait_rendering_done();
|
|
waited = 1;
|
|
}
|
|
vita2d_free_texture(g_thumb[i]);
|
|
g_thumb[i] = NULL;
|
|
}
|
|
}
|
|
|
|
/* collect finished download, then hand the worker
|
|
* the next most useful thumbnail */
|
|
if (g_req_state == REQ_DONE) {
|
|
__sync_synchronize();
|
|
consume_worker_result();
|
|
}
|
|
if (g_req_state == REQ_IDLE) {
|
|
int next = pick_next_load(sel, first_vis, last_vis);
|
|
if (next >= 0) {
|
|
g_req_idx = next;
|
|
__sync_synchronize();
|
|
g_req_state = REQ_PENDING;
|
|
}
|
|
}
|
|
|
|
vita2d_start_drawing();
|
|
vita2d_clear_screen();
|
|
|
|
for (int i = first_vis; i >= 0 && i <= last_vis; i++) {
|
|
float x = (float)(i % COLS) * CELL_W;
|
|
float y = (float)(i / COLS) * CELL_H - scroll;
|
|
float bx = x + CELL_PAD, by = y + CELL_PAD;
|
|
float bw = CELL_W - 2 * CELL_PAD;
|
|
float bh = CELL_H - 2 * CELL_PAD;
|
|
|
|
if (g_thumb[i])
|
|
draw_texture_fitted(g_thumb[i], bx, by, bw, bh);
|
|
else if (g_thumb_failed[i])
|
|
vita2d_draw_rectangle(bx, by, bw, bh,
|
|
RGBA8(90, 30, 30, 255));
|
|
else
|
|
vita2d_draw_rectangle(bx, by, bw, bh,
|
|
RGBA8(40, 40, 40, 255));
|
|
if (i == sel)
|
|
draw_sel_outline(x + 2, y + 2,
|
|
CELL_W - 4, CELL_H - 4);
|
|
}
|
|
|
|
char hud[160];
|
|
snprintf(hud, sizeof(hud),
|
|
"%d / %d%s %s X view START exit",
|
|
sel + 1, g_asset_count, g_next_page > 0 ? "+" : "",
|
|
g_asset_dates[sel]);
|
|
draw_hud(hud);
|
|
|
|
vita2d_end_drawing();
|
|
vita2d_swap_buffers();
|
|
} else { /* MODE_DETAIL */
|
|
if (nav & SCE_CTRL_RIGHT && sel < g_asset_count - 1)
|
|
sel++;
|
|
if (nav & SCE_CTRL_LEFT && sel > 0)
|
|
sel--;
|
|
if (pressed & SCE_CTRL_CIRCLE) {
|
|
mode = MODE_GRID;
|
|
continue;
|
|
}
|
|
if ((pressed & SCE_CTRL_CROSS) && detail_failed)
|
|
detail_idx = -1; /* retry */
|
|
|
|
if (detail_idx != sel) {
|
|
if (detail_tex) {
|
|
/* the GPU may still be drawing the
|
|
* previous frame with this texture */
|
|
vita2d_wait_rendering_done();
|
|
vita2d_free_texture(detail_tex);
|
|
detail_tex = NULL;
|
|
}
|
|
show_status("Loading photo %d/%d ...",
|
|
sel + 1, g_asset_count);
|
|
detail_tex = load_image(sel, FULL_MAX);
|
|
detail_failed = (detail_tex == NULL);
|
|
detail_idx = sel;
|
|
}
|
|
|
|
vita2d_start_drawing();
|
|
vita2d_clear_screen();
|
|
|
|
if (detail_tex)
|
|
draw_texture_fitted(detail_tex, 0, 0,
|
|
SCREEN_W, SCREEN_H);
|
|
else
|
|
draw_error_detail(sel);
|
|
|
|
char hud[160];
|
|
snprintf(hud, sizeof(hud),
|
|
"%d / %d %s < > browse %sO back START exit",
|
|
sel + 1, g_asset_count, g_asset_dates[sel],
|
|
detail_failed ? "X retry " : "");
|
|
draw_hud(hud);
|
|
|
|
vita2d_end_drawing();
|
|
vita2d_swap_buffers();
|
|
}
|
|
}
|
|
|
|
vita2d_wait_rendering_done();
|
|
if (detail_tex)
|
|
vita2d_free_texture(detail_tex);
|
|
for (int i = 0; i < g_asset_count; i++)
|
|
if (g_thumb[i])
|
|
vita2d_free_texture(g_thumb[i]);
|
|
vita2d_free_pgf(g_font);
|
|
vita2d_fini();
|
|
curl_global_cleanup();
|
|
sceKernelExitProcess(0);
|
|
return 0;
|
|
}
|