Justified grid, photo swipe navigation, video posters, smart prefetch
Grid: - justified rows like the Immich web timeline: widths follow each photo's exif aspect ratio (withExif on search/metadata, orientation aware), rows stretch to span the full screen width - row-aware d-pad navigation (variable items per row) Detail view: - swipe horizontally to change photos, 1:1 with the finger, neighbor riding alongside; release past a quarter screen commits, less springs back; d-pad browsing uses the same slide animation - touch drag pans 1:1 while zoomed - neighbors prefetch (thumbs first, then full-res / video posters) so swipes land sharp; opening a cold item loads its low-res thumb before the full-res - videos show their poster with a translucent play button instead of a "press X to play" text; tap or X plays; the gallery opens this screen rather than auto-playing Video startup: - the poster stays on screen through download progress, AVPlayer startup and the pre-first-frame gap; no more black screen Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+555
-95
@@ -52,10 +52,9 @@
|
||||
#define SCREEN_W 960
|
||||
#define SCREEN_H 544
|
||||
|
||||
#define COLS 4
|
||||
#define CELL_W 240
|
||||
#define CELL_H 240
|
||||
#define CELL_PAD 6
|
||||
#define COLS 5 /* rough items per row, for prefetch distances only */
|
||||
#define ROW_H 200 /* justified-grid row height; widths follow aspect */
|
||||
#define CELL_PAD 3
|
||||
#define HEADER_H 40 /* month/year band height in the grid */
|
||||
#define THUMB_MAX 256 /* decode grid thumbs down to <= this dimension */
|
||||
#define FULL_MAX 4096 /* GXM texture size limit */
|
||||
@@ -91,6 +90,7 @@ static vita2d_pgf *g_font;
|
||||
* buffers), so a realloc here can't dangle a pointer under it. */
|
||||
static char (*g_asset_ids)[40];
|
||||
static char (*g_asset_dates)[DATELEN]; /* YYYY-MM-DDTHH:MM:SS */
|
||||
static float *g_asset_ratio; /* display w/h from exif, 0 = unknown */
|
||||
static unsigned char *g_asset_is_video;
|
||||
/* set during display rebuild: a backed-up local file matches this server
|
||||
* asset, so its grid cell shows the green "backed up" badge */
|
||||
@@ -147,10 +147,14 @@ 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 of the
|
||||
* cell, plus the month/year section headers shown above each month's run */
|
||||
/* 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;
|
||||
@@ -214,6 +218,7 @@ static int grow_assets(int need)
|
||||
return 1;
|
||||
if (!GROW(g_asset_ids, c, need) ||
|
||||
!GROW(g_asset_dates, c, need) ||
|
||||
!GROW(g_asset_ratio, c, need) ||
|
||||
!GROW(g_asset_is_video, c, need) ||
|
||||
!GROW(g_asset_local_backed, c, need) ||
|
||||
!GROW(g_thumb, c, need) ||
|
||||
@@ -253,6 +258,8 @@ static int grow_disp(int need)
|
||||
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;
|
||||
@@ -787,30 +794,57 @@ static int parse_assets(const char *js, size_t jslen, char *err, size_t errlen)
|
||||
}
|
||||
|
||||
/* 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) */
|
||||
* top-level object belong to that asset (exifInfo is the one nested
|
||||
* object we descend into, for the photo's display aspect ratio) */
|
||||
int added = 0;
|
||||
int cur = -1, cur_obj = -1;
|
||||
int cur = -1, cur_obj = -1, exif_obj = -1;
|
||||
int exw = 0, exh = 0, exori = 0;
|
||||
for (int i = arr_idx + 1; i < ntok; i++) {
|
||||
if (tok[i].type == JSMN_OBJECT && tok[i].parent == arr_idx) {
|
||||
if (cur >= 0 && exw > 0 && exh > 0)
|
||||
g_asset_ratio[cur] = (exori >= 5 && exori <= 8) ?
|
||||
(float)exh / exw : (float)exw / exh;
|
||||
if (!grow_assets(g_asset_count + 1))
|
||||
break; /* out of memory: keep what we have */
|
||||
cur = g_asset_count++;
|
||||
cur_obj = i;
|
||||
exif_obj = -1;
|
||||
exw = exh = exori = 0;
|
||||
added++;
|
||||
g_asset_ids[cur][0] = '\0';
|
||||
g_asset_dates[cur][0] = '\0';
|
||||
g_asset_ratio[cur] = 0.0f;
|
||||
g_asset_is_video[cur] = 0;
|
||||
continue;
|
||||
}
|
||||
if (cur < 0 || tok[i].type != JSMN_STRING || tok[i].size != 1 ||
|
||||
tok[i].parent != cur_obj || i + 1 >= ntok)
|
||||
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 (tok[i].parent == exif_obj) {
|
||||
/* numbers may also arrive as strings; atoi covers both
|
||||
* and yields 0 on null */
|
||||
if (klen == 14 && !strncmp(k, "exifImageWidth", 14))
|
||||
exw = atoi(js + val->start);
|
||||
else if (klen == 15 && !strncmp(k, "exifImageHeight", 15))
|
||||
exh = atoi(js + val->start);
|
||||
else if (klen == 11 && !strncmp(k, "orientation", 11))
|
||||
exori = atoi(js + val->start);
|
||||
continue;
|
||||
}
|
||||
if (tok[i].parent != cur_obj)
|
||||
continue;
|
||||
|
||||
if (klen == 8 && !strncmp(k, "exifInfo", 8) &&
|
||||
val->type == JSMN_OBJECT) {
|
||||
exif_obj = i + 1;
|
||||
continue;
|
||||
}
|
||||
if (val->type != JSMN_STRING)
|
||||
continue;
|
||||
|
||||
@@ -828,6 +862,9 @@ static int parse_assets(const char *js, size_t jslen, char *err, size_t errlen)
|
||||
g_asset_is_video[cur] =
|
||||
(vlen == 5 && !strncmp(js + val->start, "VIDEO", 5));
|
||||
}
|
||||
if (cur >= 0 && exw > 0 && exh > 0)
|
||||
g_asset_ratio[cur] = (exori >= 5 && exori <= 8) ?
|
||||
(float)exh / exw : (float)exw / exh;
|
||||
free(tok);
|
||||
|
||||
/* drop trailing entries that somehow lack an id */
|
||||
@@ -848,7 +885,7 @@ static int fetch_page(int is_first)
|
||||
snprintf(url, sizeof(url), "%s/api/search/metadata", g_server);
|
||||
char body[128];
|
||||
snprintf(body, sizeof(body),
|
||||
"{\"page\":%d,\"size\":%d,\"order\":\"desc\"}",
|
||||
"{\"page\":%d,\"size\":%d,\"order\":\"desc\",\"withExif\":true}",
|
||||
g_next_page, PAGE_SIZE);
|
||||
|
||||
membuf buf;
|
||||
@@ -890,7 +927,11 @@ static int fetch_page(int is_first)
|
||||
|
||||
/* 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);
|
||||
int with_ratio = 0;
|
||||
for (int i = g_asset_count - added; i < g_asset_count; i++)
|
||||
with_ratio += (g_asset_ratio[i] > 0.0f);
|
||||
log_line("page fetched: +%d assets (total %d, %d with exif ratio)",
|
||||
added, g_asset_count, with_ratio);
|
||||
return added;
|
||||
}
|
||||
|
||||
@@ -1291,14 +1332,51 @@ static void month_label(const char *date, char *out, size_t len)
|
||||
snprintf(out, len, "%s %d", mon[m], y);
|
||||
}
|
||||
|
||||
/* compute each display slot's grid cell + the month/year header bands. the
|
||||
* timeline is already date-desc sorted, so a run of equal "YYYY-MM" is one
|
||||
* month; every month starts on a fresh row under its own header. */
|
||||
/* display aspect ratio (w/h) of a slot; local camera media and assets the
|
||||
* server reported no exif for fall back to 4:3 */
|
||||
static float disp_ratio(int d)
|
||||
{
|
||||
struct disp_item *it = &g_disp[d];
|
||||
float r = (it->src == SRC_SERVER) ? g_asset_ratio[it->idx] : 0.0f;
|
||||
return r > 0.0f ? r : 4.0f / 3.0f;
|
||||
}
|
||||
|
||||
/* place row items [start, end) at *y. when justifying, the whole row is
|
||||
* scaled so it spans the full screen width (the row height grows with it,
|
||||
* capped so a sparse row can't blow up); a month's trailing partial row
|
||||
* stays at its natural size, left-aligned. */
|
||||
static void layout_finish_row(int start, int end, float *y, float natw,
|
||||
int justify)
|
||||
{
|
||||
float scale = 1.0f;
|
||||
if (justify && natw > 0.0f) {
|
||||
scale = (float)SCREEN_W / natw;
|
||||
if (scale > 1.55f)
|
||||
scale = 1.55f;
|
||||
}
|
||||
float h = ROW_H * scale;
|
||||
float x = 0.0f;
|
||||
for (int i = start; i < end; i++) {
|
||||
g_item_x[i] = x;
|
||||
g_item_y[i] = *y;
|
||||
g_item_w[i] *= scale;
|
||||
g_item_h[i] = h;
|
||||
x += g_item_w[i];
|
||||
}
|
||||
*y += h;
|
||||
}
|
||||
|
||||
/* compute each display slot's grid cell + the month/year header bands.
|
||||
* justified rows like the Immich web timeline: each item's width follows
|
||||
* its aspect ratio, a row wraps when the next item no longer fits, and the
|
||||
* closed row is stretched to fill the full screen width. the timeline is
|
||||
* date-desc sorted, so a run of equal "YYYY-MM" is one month; every month
|
||||
* starts on a fresh row under its own header. */
|
||||
static void layout_grid(void)
|
||||
{
|
||||
g_sect_count = 0;
|
||||
float y = 0.0f;
|
||||
int col = 0;
|
||||
float y = 0.0f, natw = 0.0f;
|
||||
int row_start = 0;
|
||||
char curkey[8] = "";
|
||||
|
||||
for (int d = 0; d < g_disp_count; d++) {
|
||||
@@ -1307,10 +1385,10 @@ static void layout_grid(void)
|
||||
snprintf(key, sizeof(key), "%.7s", date); /* YYYY-MM */
|
||||
|
||||
if (strcmp(key, curkey) != 0) {
|
||||
if (col != 0) { /* finish the partial last row */
|
||||
y += CELL_H;
|
||||
col = 0;
|
||||
}
|
||||
if (d > row_start) /* month's partial last row */
|
||||
layout_finish_row(row_start, d, &y, natw, 0);
|
||||
row_start = d;
|
||||
natw = 0.0f;
|
||||
if (grow_sect(g_sect_count + 1)) {
|
||||
g_sect[g_sect_count].y = y;
|
||||
month_label(date, g_sect[g_sect_count].label,
|
||||
@@ -1321,15 +1399,21 @@ static void layout_grid(void)
|
||||
snprintf(curkey, sizeof(curkey), "%s", key);
|
||||
}
|
||||
|
||||
g_item_x[d] = (float)col * CELL_W;
|
||||
g_item_y[d] = y;
|
||||
if (++col == COLS) {
|
||||
col = 0;
|
||||
y += CELL_H;
|
||||
float w = ROW_H * disp_ratio(d);
|
||||
if (w < ROW_H * 0.4f)
|
||||
w = ROW_H * 0.4f; /* keep extreme portraits tappable */
|
||||
if (w > SCREEN_W)
|
||||
w = SCREEN_W; /* panoramas: one per row */
|
||||
if (natw > 0.0f && natw + w > SCREEN_W) {
|
||||
layout_finish_row(row_start, d, &y, natw, 1);
|
||||
row_start = d;
|
||||
natw = 0.0f;
|
||||
}
|
||||
g_item_w[d] = w; /* natural width; scaled when the row closes */
|
||||
natw += w;
|
||||
}
|
||||
if (col != 0)
|
||||
y += CELL_H;
|
||||
if (g_disp_count > row_start)
|
||||
layout_finish_row(row_start, g_disp_count, &y, natw, 0);
|
||||
g_content_h = y;
|
||||
}
|
||||
|
||||
@@ -1411,20 +1495,52 @@ static int item_at(float x, float wy)
|
||||
for (int i = 0; i < g_disp_count; i++) {
|
||||
if (g_item_y[i] > wy)
|
||||
break; /* item y is non-decreasing */
|
||||
if (wy < g_item_y[i] + CELL_H &&
|
||||
x >= g_item_x[i] && x < g_item_x[i] + CELL_W)
|
||||
if (wy < g_item_y[i] + g_item_h[i] &&
|
||||
x >= g_item_x[i] && x < g_item_x[i] + g_item_w[i])
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* move the selection one row up (dir<0) or down (dir>0), landing on the
|
||||
* item whose cell is horizontally nearest — rows hold a variable number
|
||||
* of items now, so the fixed columns arithmetic no longer applies */
|
||||
static int nav_row(int sel, int dir)
|
||||
{
|
||||
if (sel < 0 || sel >= g_disp_count)
|
||||
return sel;
|
||||
float cy = g_item_y[sel];
|
||||
float cx = g_item_x[sel] + g_item_w[sel] / 2.0f;
|
||||
int i = sel;
|
||||
/* step off the current row */
|
||||
while (i + dir >= 0 && i + dir < g_disp_count &&
|
||||
g_item_y[i] == cy)
|
||||
i += dir;
|
||||
if (g_item_y[i] == cy)
|
||||
return sel; /* already on the first/last row */
|
||||
/* pick the horizontally nearest item of that row */
|
||||
float ry = g_item_y[i];
|
||||
int best = i;
|
||||
float bestd = -1.0f;
|
||||
for (; i >= 0 && i < g_disp_count && g_item_y[i] == ry; i += dir) {
|
||||
float d = g_item_x[i] + g_item_w[i] / 2.0f - cx;
|
||||
if (d < 0)
|
||||
d = -d;
|
||||
if (bestd < 0 || d < bestd) {
|
||||
bestd = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/* display slot nearest a world-y (used to keep the selection inside the
|
||||
* viewport while a touch drag/fling moves the grid) */
|
||||
static int item_near(float wy)
|
||||
{
|
||||
int last = -1;
|
||||
for (int i = 0; i < g_disp_count; i++) {
|
||||
if (g_item_y[i] + CELL_H > wy)
|
||||
if (g_item_y[i] + g_item_h[i] > wy)
|
||||
return i;
|
||||
last = i;
|
||||
}
|
||||
@@ -1459,6 +1575,27 @@ static int pick_next_load(int sel, int first_vis, int last_vis)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* hand the worker a thumbnail request for display slot d (g_req_state must
|
||||
* be REQ_IDLE); local videos get their poster extracted */
|
||||
static void req_issue_thumb(int d)
|
||||
{
|
||||
struct disp_item *ti = &g_disp[d];
|
||||
g_req_idx = ti->idx;
|
||||
g_req_src = ti->src;
|
||||
g_req_detail = 0;
|
||||
if (ti->src == SRC_LOCAL) {
|
||||
snprintf(g_req_path, sizeof(g_req_path), "%s",
|
||||
g_local_path[ti->idx]);
|
||||
g_req_is_video = g_local_is_video[ti->idx];
|
||||
} else {
|
||||
snprintf(g_req_id, sizeof(g_req_id), "%s",
|
||||
g_asset_ids[ti->idx]);
|
||||
g_req_is_video = 0;
|
||||
}
|
||||
__sync_synchronize();
|
||||
g_req_state = REQ_PENDING;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* UI */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -1519,6 +1656,22 @@ static void draw_hud(const char *text)
|
||||
RGBA8(255, 255, 255, 255), 1.0f, text);
|
||||
}
|
||||
|
||||
/* translucent play button over a video's poster in the detail view */
|
||||
static void draw_play_overlay(void)
|
||||
{
|
||||
float cx = SCREEN_W / 2.0f, cy = SCREEN_H / 2.0f;
|
||||
vita2d_draw_fill_circle(cx, cy, 46.0f, RGBA8(20, 20, 20, 150));
|
||||
vita2d_color_vertex *v =
|
||||
vita2d_pool_memalign(3 * sizeof(*v), sizeof(*v));
|
||||
if (!v)
|
||||
return;
|
||||
unsigned int col = RGBA8(255, 255, 255, 220);
|
||||
v[0] = (vita2d_color_vertex){ cx - 14.0f, cy - 24.0f, 0.5f, col };
|
||||
v[1] = (vita2d_color_vertex){ cx - 14.0f, cy + 24.0f, 0.5f, col };
|
||||
v[2] = (vita2d_color_vertex){ cx + 28.0f, cy, 0.5f, col };
|
||||
vita2d_draw_array(SCE_GXM_PRIMITIVE_TRIANGLES, v, 3);
|
||||
}
|
||||
|
||||
static void draw_error_detail(int idx)
|
||||
{
|
||||
draw_centered(220, RGBA8(255, 80, 80, 255), "Failed to load this photo");
|
||||
@@ -1570,6 +1723,25 @@ static size_t file_write_cb(void *ptr, size_t size, size_t nmemb, void *ud)
|
||||
}
|
||||
|
||||
static uint64_t g_dl_last_draw;
|
||||
/* poster/thumb drawn behind the download progress and the player's startup
|
||||
* (the seconds AVPlayer spends parsing before the first frame); set by the
|
||||
* play call sites, valid for the whole modal download + playback */
|
||||
static vita2d_texture *g_dl_bg;
|
||||
|
||||
/* full-screen status with the video's poster behind it */
|
||||
static void show_video_status(const char *text)
|
||||
{
|
||||
vita2d_start_drawing();
|
||||
vita2d_clear_screen();
|
||||
if (g_dl_bg) {
|
||||
draw_texture_fitted(g_dl_bg, 0, 0, SCREEN_W, SCREEN_H);
|
||||
vita2d_draw_rectangle(0, 0, SCREEN_W, SCREEN_H,
|
||||
RGBA8(0, 0, 0, 130));
|
||||
}
|
||||
draw_centered(SCREEN_H / 2, RGBA8(255, 255, 255, 255), text);
|
||||
vita2d_end_drawing();
|
||||
vita2d_swap_buffers();
|
||||
}
|
||||
|
||||
static int dl_progress_cb(void *ud, curl_off_t dltotal, curl_off_t dlnow,
|
||||
curl_off_t ultotal, curl_off_t ulnow)
|
||||
@@ -1595,6 +1767,12 @@ static int dl_progress_cb(void *ud, curl_off_t dltotal, curl_off_t dlnow,
|
||||
|
||||
vita2d_start_drawing();
|
||||
vita2d_clear_screen();
|
||||
if (g_dl_bg) {
|
||||
/* the video's poster behind the progress, dimmed */
|
||||
draw_texture_fitted(g_dl_bg, 0, 0, SCREEN_W, SCREEN_H);
|
||||
vita2d_draw_rectangle(0, 0, SCREEN_W, SCREEN_H,
|
||||
RGBA8(0, 0, 0, 130));
|
||||
}
|
||||
draw_centered(SCREEN_H / 2 - 20, RGBA8(255, 255, 255, 255), buf);
|
||||
if (dltotal > 0) {
|
||||
float frac = (float)dlnow / (float)dltotal;
|
||||
@@ -1931,7 +2109,7 @@ static void play_video_file(const char *path)
|
||||
g_player_active = 1;
|
||||
__sync_synchronize();
|
||||
for (int i = 0; i < 720 && g_poster_active; i++)
|
||||
show_status("Starting video...");
|
||||
show_video_status("Starting video...");
|
||||
|
||||
/* replay re-inits the player from the same (already-local) file, so the
|
||||
* decoder claim above is taken once and held across replays. */
|
||||
@@ -1988,7 +2166,7 @@ replay:
|
||||
/* AddSource parses asynchronously; give it ~5 s to start */
|
||||
int active = 0;
|
||||
for (int i = 0; i < 300 && !(active = sceAvPlayerIsActive(g_avp)); i++)
|
||||
show_status("Starting video...");
|
||||
show_video_status("Starting video...");
|
||||
|
||||
/* AddSource parses asynchronously, so the stream duration is usually not
|
||||
* available yet at this point; keep re-querying it in the loop below until
|
||||
@@ -2056,6 +2234,9 @@ replay:
|
||||
vita2d_clear_screen();
|
||||
if (cur)
|
||||
draw_texture_fitted(cur, 0, 0, SCREEN_W, SCREEN_H);
|
||||
else if (g_dl_bg)
|
||||
/* poster until the first decoded frame arrives */
|
||||
draw_texture_fitted(g_dl_bg, 0, 0, SCREEN_W, SCREEN_H);
|
||||
|
||||
if (duration == 0) {
|
||||
SceAvPlayerStreamInfo sinfo;
|
||||
@@ -2136,6 +2317,12 @@ replay:
|
||||
/* download + play asset idx, cleaning up the temp file afterwards */
|
||||
static void view_video(int idx)
|
||||
{
|
||||
/* the video's poster behind the download progress; the detail view
|
||||
* pre-sets its sharper full-res poster, the grid falls back to the
|
||||
* asset thumb. the modal download keeps the texture alive. */
|
||||
if (!g_dl_bg)
|
||||
g_dl_bg = g_thumb[idx];
|
||||
|
||||
char err[160];
|
||||
int r = download_video(idx, err, sizeof(err));
|
||||
if (r == 0) {
|
||||
@@ -2145,6 +2332,7 @@ static void view_video(int idx)
|
||||
log_line("video %s: %s", g_asset_ids[idx], err);
|
||||
show_blocking_error("Failed to download video", err);
|
||||
}
|
||||
g_dl_bg = NULL;
|
||||
sceIoRemove(VIDEO_TMP_PATH);
|
||||
}
|
||||
|
||||
@@ -2864,13 +3052,29 @@ int main(void)
|
||||
int detail_issued = 0; /* the worker request has been handed over */
|
||||
float zoom = 1.0f, panx = 0.0f, pany = 0.0f;
|
||||
|
||||
/* front-touch state (grid: drag scrolls, tap opens) */
|
||||
/* front-touch state (grid: drag scrolls, tap opens; detail: swipe
|
||||
* changes photo, drag pans when zoomed) */
|
||||
int touch_active = 0; /* finger currently down */
|
||||
int touch_dragged = 0; /* moved past the tap threshold */
|
||||
float touch_x = 0, touch_y = 0; /* last position, screen px */
|
||||
float touch_start_y = 0, touch_start_scroll = 0;
|
||||
float touch_start_x = 0, touch_start_y = 0, touch_start_scroll = 0;
|
||||
float touch_panx0 = 0, touch_pany0 = 0; /* pan at touch start (zoomed) */
|
||||
float touch_vel = 0; /* px/frame at the moment of release */
|
||||
|
||||
/* detail-view photo slide: the current photo's horizontal offset.
|
||||
* a swipe maps it 1:1 to the finger; on release (or d-pad browse) it
|
||||
* animates to +-SCREEN_W, the selection switches, and the neighbor's
|
||||
* thumbnail rides alongside the whole way. */
|
||||
float slide_x = 0, slide_goal = 0;
|
||||
int slide_anim = 0;
|
||||
|
||||
/* detail-view neighbor prefetch: while the current photo is settled,
|
||||
* the worker quietly fetches the full-res images (or video posters)
|
||||
* of sel+1 / sel-1 so browsing lands sharp instantly */
|
||||
vita2d_texture *pf_tex[2] = { NULL, NULL };
|
||||
int pf_d[2] = { -1, -1 }; /* display slot each cache slot holds */
|
||||
int pf_pending = -1; /* slot being fetched right now, -1 none */
|
||||
|
||||
for (;;) {
|
||||
SceCtrlData pad;
|
||||
sceCtrlPeekBufferPositive(0, &pad, 1);
|
||||
@@ -2898,6 +3102,16 @@ int main(void)
|
||||
if (sel >= g_disp_count)
|
||||
sel = g_disp_count - 1;
|
||||
detail_idx = -1; /* re-decode: item under sel may differ */
|
||||
/* display slots were reshuffled: the prefetch cache's
|
||||
* slot keys are meaningless now */
|
||||
for (int k = 0; k < 2; k++) {
|
||||
if (pf_tex[k]) {
|
||||
vita2d_wait_rendering_done();
|
||||
vita2d_free_texture(pf_tex[k]);
|
||||
pf_tex[k] = NULL;
|
||||
}
|
||||
pf_d[k] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* SELECT toggles the sync overview from the grid / detail view */
|
||||
@@ -2920,14 +3134,14 @@ int main(void)
|
||||
sel++;
|
||||
if (nav & SCE_CTRL_LEFT)
|
||||
sel--;
|
||||
if (nav & SCE_CTRL_DOWN)
|
||||
sel += COLS;
|
||||
if (nav & SCE_CTRL_UP)
|
||||
sel -= COLS;
|
||||
if (sel < 0)
|
||||
sel = 0;
|
||||
if (sel >= g_disp_count)
|
||||
sel = g_disp_count - 1;
|
||||
if (nav & SCE_CTRL_DOWN)
|
||||
sel = nav_row(sel, +1);
|
||||
if (nav & SCE_CTRL_UP)
|
||||
sel = nav_row(sel, -1);
|
||||
if (pressed & SCE_CTRL_RTRIGGER)
|
||||
sel = month_jump(sel, +1); /* older month */
|
||||
if (pressed & SCE_CTRL_LTRIGGER)
|
||||
@@ -2953,27 +3167,14 @@ int main(void)
|
||||
int scrolling_fast = (grid_settle < 12);
|
||||
|
||||
if (pressed & SCE_CTRL_CROSS) {
|
||||
if (g_disp_count > 0 && disp_is_video(sel)) {
|
||||
/* videos start loading/playing right
|
||||
* away; no "press X to play" stop */
|
||||
struct disp_item it = g_disp[sel];
|
||||
if (it.src == SRC_LOCAL)
|
||||
play_video_file(g_local_path[it.idx]);
|
||||
else
|
||||
view_video(it.idx);
|
||||
/* wait for the player's buttons to be
|
||||
* released so they don't also act on
|
||||
* the grid */
|
||||
do {
|
||||
sceCtrlPeekBufferPositive(0, &pad, 1);
|
||||
sceKernelDelayThread(10 * 1000);
|
||||
} while (pad.buttons);
|
||||
prev_buttons = 0;
|
||||
continue;
|
||||
}
|
||||
/* photos and videos both open the detail
|
||||
* view; a video shows its poster with the
|
||||
* play button there */
|
||||
mode = MODE_DETAIL;
|
||||
zoom = 1.0f;
|
||||
panx = pany = 0.0f;
|
||||
slide_x = slide_goal = 0;
|
||||
slide_anim = 0;
|
||||
}
|
||||
|
||||
/* fetch the next page when the selection settles near the
|
||||
@@ -3031,27 +3232,18 @@ int main(void)
|
||||
} else if (touch_active) {
|
||||
touch_active = 0;
|
||||
if (!touch_dragged) {
|
||||
/* tap: open the item under it */
|
||||
/* tap: open the item under it
|
||||
* (videos show their poster +
|
||||
* play button in the detail) */
|
||||
int i = item_at(touch_x,
|
||||
touch_y + scroll);
|
||||
if (i >= 0) {
|
||||
sel = i;
|
||||
if (disp_is_video(i)) {
|
||||
struct disp_item ti = g_disp[i];
|
||||
if (ti.src == SRC_LOCAL)
|
||||
play_video_file(g_local_path[ti.idx]);
|
||||
else
|
||||
view_video(ti.idx);
|
||||
do {
|
||||
sceCtrlPeekBufferPositive(0, &pad, 1);
|
||||
sceKernelDelayThread(10 * 1000);
|
||||
} while (pad.buttons);
|
||||
prev_buttons = 0;
|
||||
continue;
|
||||
}
|
||||
mode = MODE_DETAIL;
|
||||
zoom = 1.0f;
|
||||
panx = pany = 0.0f;
|
||||
slide_x = slide_goal = 0;
|
||||
slide_anim = 0;
|
||||
}
|
||||
} else if (touch_vel > 2.0f ||
|
||||
touch_vel < -2.0f) {
|
||||
@@ -3078,8 +3270,10 @@ int main(void)
|
||||
g_item_y[sel] : 0.0f;
|
||||
if (sel_y - HEADER_H < target)
|
||||
target = sel_y - HEADER_H;
|
||||
if (sel_y + CELL_H > target + SCREEN_H)
|
||||
target = sel_y + CELL_H - SCREEN_H;
|
||||
float sel_h = (sel >= 0 && sel < g_disp_count) ?
|
||||
g_item_h[sel] : ROW_H;
|
||||
if (sel_y + sel_h > target + SCREEN_H)
|
||||
target = sel_y + sel_h - SCREEN_H;
|
||||
float max_scroll = g_content_h - SCREEN_H;
|
||||
if (max_scroll < 0)
|
||||
max_scroll = 0;
|
||||
@@ -3098,7 +3292,7 @@ int main(void)
|
||||
* are recycled through the pool, see tex_release */
|
||||
for (int i = 0; i < g_disp_count; i++) {
|
||||
float dy = g_item_y[i] - scroll;
|
||||
if (dy > -CELL_H && dy < SCREEN_H) {
|
||||
if (dy > -g_item_h[i] && dy < SCREEN_H) {
|
||||
if (first_vis < 0)
|
||||
first_vis = i;
|
||||
last_vis = i;
|
||||
@@ -3178,8 +3372,8 @@ int main(void)
|
||||
float x = g_item_x[i];
|
||||
float y = g_item_y[i] - scroll;
|
||||
float bx = x + CELL_PAD, by = y + CELL_PAD;
|
||||
float bw = CELL_W - 2 * CELL_PAD;
|
||||
float bh = CELL_H - 2 * CELL_PAD;
|
||||
float bw = g_item_w[i] - 2 * CELL_PAD;
|
||||
float bh = g_item_h[i] - 2 * CELL_PAD;
|
||||
|
||||
vita2d_texture *th = disp_thumb(i);
|
||||
if (th)
|
||||
@@ -3202,7 +3396,8 @@ int main(void)
|
||||
draw_status_badge(bx, by, i, frame);
|
||||
if (i == sel)
|
||||
draw_sel_outline(x + 2, y + 2,
|
||||
CELL_W - 4, CELL_H - 4);
|
||||
g_item_w[i] - 4,
|
||||
g_item_h[i] - 4);
|
||||
}
|
||||
|
||||
char hud[160];
|
||||
@@ -3246,11 +3441,106 @@ int main(void)
|
||||
if (pad.buttons & SCE_CTRL_LEFT) panx += 12.0f;
|
||||
if (pad.buttons & SCE_CTRL_UP) pany += 12.0f;
|
||||
if (pad.buttons & SCE_CTRL_DOWN) pany -= 12.0f;
|
||||
} else if (!slide_anim && !touch_active) {
|
||||
/* browse with a slide animation; the selection
|
||||
* switches when the slide completes */
|
||||
if (nav & SCE_CTRL_RIGHT && sel < g_disp_count - 1) {
|
||||
slide_anim = 1;
|
||||
slide_goal = -SCREEN_W;
|
||||
}
|
||||
if (nav & SCE_CTRL_LEFT && sel > 0) {
|
||||
slide_anim = 1;
|
||||
slide_goal = SCREEN_W;
|
||||
}
|
||||
}
|
||||
|
||||
/* front touch: when zoomed a drag pans the photo 1:1;
|
||||
* otherwise a horizontal swipe drags the photo (and its
|
||||
* neighbor) 1:1 and a release past the threshold flips
|
||||
* to it, anything less springs back */
|
||||
{
|
||||
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;
|
||||
touch_panx0 = panx;
|
||||
touch_pany0 = pany;
|
||||
} else {
|
||||
if (nav & SCE_CTRL_RIGHT && sel < g_disp_count - 1)
|
||||
float dx = tx - touch_start_x;
|
||||
float dy = ty - touch_start_y;
|
||||
if (!touch_dragged &&
|
||||
(dx > 12.0f || dx < -12.0f ||
|
||||
dy > 12.0f || dy < -12.0f))
|
||||
touch_dragged = 1;
|
||||
if (touch_dragged && zoomed) {
|
||||
panx = touch_panx0 + dx;
|
||||
pany = touch_pany0 + dy;
|
||||
} else if (touch_dragged &&
|
||||
!slide_anim) {
|
||||
slide_x = dx;
|
||||
/* nothing beyond the ends */
|
||||
if (sel <= 0 && slide_x > 0)
|
||||
slide_x = 0;
|
||||
if (sel >= g_disp_count - 1 &&
|
||||
slide_x < 0)
|
||||
slide_x = 0;
|
||||
}
|
||||
}
|
||||
touch_x = tx;
|
||||
touch_y = ty;
|
||||
} else if (touch_active) {
|
||||
touch_active = 0;
|
||||
if (touch_dragged && !zoomed && !slide_anim) {
|
||||
if (slide_x < -SCREEN_W / 4.0f &&
|
||||
sel < g_disp_count - 1)
|
||||
slide_goal = -SCREEN_W;
|
||||
else if (slide_x > SCREEN_W / 4.0f &&
|
||||
sel > 0)
|
||||
slide_goal = SCREEN_W;
|
||||
else
|
||||
slide_goal = 0; /* spring back */
|
||||
slide_anim = 1;
|
||||
} else if (!touch_dragged && is_video &&
|
||||
!slide_anim) {
|
||||
/* tap on the poster plays it */
|
||||
g_dl_bg = detail_tex ?
|
||||
detail_tex :
|
||||
disp_thumb(sel);
|
||||
if (is_local)
|
||||
play_video_file(g_local_path[it.idx]);
|
||||
else
|
||||
view_video(it.idx);
|
||||
g_dl_bg = NULL;
|
||||
do {
|
||||
sceCtrlPeekBufferPositive(0, &pad, 1);
|
||||
sceKernelDelayThread(10 * 1000);
|
||||
} while (pad.buttons);
|
||||
prev_buttons = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* run the slide animation; at the end the selection
|
||||
* moves and the (centered) neighbor becomes current */
|
||||
if (slide_anim && !touch_active) {
|
||||
slide_x += (slide_goal - slide_x) * 0.35f;
|
||||
float dd = slide_goal - slide_x;
|
||||
if (dd < 4.0f && dd > -4.0f) {
|
||||
if (slide_goal < -1.0f && sel < g_disp_count - 1)
|
||||
sel++;
|
||||
if (nav & SCE_CTRL_LEFT && sel > 0)
|
||||
else if (slide_goal > 1.0f && sel > 0)
|
||||
sel--;
|
||||
slide_x = 0;
|
||||
slide_goal = 0;
|
||||
slide_anim = 0;
|
||||
}
|
||||
}
|
||||
if (pressed & SCE_CTRL_CIRCLE) {
|
||||
/* back to the grid; if a full-res load is in
|
||||
@@ -3258,16 +3548,28 @@ int main(void)
|
||||
mode = MODE_GRID;
|
||||
detail_idx = -1;
|
||||
detail_loading = 0;
|
||||
slide_x = slide_goal = 0;
|
||||
slide_anim = 0;
|
||||
for (int k = 0; k < 2; k++) {
|
||||
if (pf_tex[k]) {
|
||||
vita2d_wait_rendering_done();
|
||||
vita2d_free_texture(pf_tex[k]);
|
||||
pf_tex[k] = NULL;
|
||||
}
|
||||
pf_d[k] = -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (pressed & SCE_CTRL_CROSS) {
|
||||
if (detail_failed) {
|
||||
detail_idx = -1; /* retry */
|
||||
} else if (is_video) {
|
||||
if (is_video) {
|
||||
/* play even if the poster fetch failed */
|
||||
g_dl_bg = detail_tex ?
|
||||
detail_tex : disp_thumb(sel);
|
||||
if (is_local)
|
||||
play_video_file(g_local_path[it.idx]);
|
||||
else
|
||||
view_video(it.idx);
|
||||
g_dl_bg = NULL;
|
||||
/* wait for the buttons used inside the
|
||||
* player to be released, so they don't
|
||||
* also act on this screen */
|
||||
@@ -3278,6 +3580,8 @@ int main(void)
|
||||
prev_buttons = 0;
|
||||
continue;
|
||||
}
|
||||
if (detail_failed)
|
||||
detail_idx = -1; /* retry */
|
||||
}
|
||||
|
||||
/* left/right may have moved the selection; refresh the
|
||||
@@ -3287,6 +3591,29 @@ int main(void)
|
||||
is_local = (it.src == SRC_LOCAL);
|
||||
is_video = disp_is_video(sel);
|
||||
|
||||
/* collect a finished neighbor prefetch (and drop it
|
||||
* if browsing already moved past its neighborhood) */
|
||||
if (pf_pending >= 0 && g_req_state == REQ_DONE) {
|
||||
__sync_synchronize();
|
||||
vita2d_texture *t = consume_detail_result();
|
||||
int keep = (pf_pending == sel - 1 ||
|
||||
pf_pending == sel + 1 ||
|
||||
pf_pending == sel);
|
||||
if (t && keep) {
|
||||
int slot = pf_tex[0] ? 1 : 0;
|
||||
if (pf_tex[slot]) {
|
||||
vita2d_wait_rendering_done();
|
||||
vita2d_free_texture(pf_tex[slot]);
|
||||
}
|
||||
pf_tex[slot] = t;
|
||||
pf_d[slot] = pf_pending;
|
||||
} else if (t) {
|
||||
vita2d_wait_rendering_done();
|
||||
vita2d_free_texture(t);
|
||||
}
|
||||
pf_pending = -1;
|
||||
}
|
||||
|
||||
if (detail_idx != sel) {
|
||||
zoom = 1.0f; /* reset view for the new photo */
|
||||
panx = pany = 0.0f;
|
||||
@@ -3298,22 +3625,53 @@ int main(void)
|
||||
detail_tex = NULL;
|
||||
}
|
||||
detail_failed = 0;
|
||||
/* photos load asynchronously on the worker;
|
||||
* the blown-up grid thumb shows meanwhile and
|
||||
* O stays responsive (videos: X plays) */
|
||||
detail_loading = !is_video;
|
||||
/* photos (and server videos, whose poster is
|
||||
* the same preview endpoint) load async on the
|
||||
* worker; the blown-up grid thumb shows
|
||||
* meanwhile and O stays responsive */
|
||||
detail_loading = (!is_video ||
|
||||
it.src == SRC_SERVER);
|
||||
detail_issued = 0;
|
||||
detail_idx = sel;
|
||||
/* prefetched? take it and skip the load */
|
||||
for (int k = 0; k < 2; k++) {
|
||||
if (pf_d[k] == sel && pf_tex[k]) {
|
||||
detail_tex = pf_tex[k];
|
||||
pf_tex[k] = NULL;
|
||||
pf_d[k] = -1;
|
||||
detail_loading = 0;
|
||||
}
|
||||
}
|
||||
/* drop cache entries no longer adjacent */
|
||||
for (int k = 0; k < 2; k++) {
|
||||
if (pf_d[k] >= 0 &&
|
||||
(pf_d[k] < sel - 1 ||
|
||||
pf_d[k] > sel + 1)) {
|
||||
vita2d_wait_rendering_done();
|
||||
vita2d_free_texture(pf_tex[k]);
|
||||
pf_tex[k] = NULL;
|
||||
pf_d[k] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* drive the async full-res load */
|
||||
if (detail_loading) {
|
||||
if (!detail_issued) {
|
||||
if (g_req_state == REQ_DONE) {
|
||||
if (pf_pending < 0 &&
|
||||
g_req_state == REQ_DONE) {
|
||||
__sync_synchronize();
|
||||
consume_worker_result();
|
||||
}
|
||||
if (g_req_state == REQ_IDLE) {
|
||||
if (g_req_state == REQ_IDLE &&
|
||||
disp_wants_thumb(sel)) {
|
||||
/* no low-res preview at all yet
|
||||
* (opened mid-scroll): fetch the
|
||||
* quick thumb first so something
|
||||
* shows; the full-res request
|
||||
* follows right after */
|
||||
req_issue_thumb(sel);
|
||||
} else if (g_req_state == REQ_IDLE) {
|
||||
g_req_idx = it.idx;
|
||||
g_req_src = it.src;
|
||||
g_req_detail = 1;
|
||||
@@ -3336,17 +3694,116 @@ int main(void)
|
||||
detail_loading = 0;
|
||||
detail_failed = (detail_tex == NULL);
|
||||
}
|
||||
} else if (pf_pending < 0) {
|
||||
/* a stale detail result (the user moved on
|
||||
* while it was in flight) would wedge the
|
||||
* request slot: discard it */
|
||||
if (g_req_state == REQ_DONE) {
|
||||
__sync_synchronize();
|
||||
consume_worker_result();
|
||||
}
|
||||
if (g_req_state != REQ_IDLE ||
|
||||
slide_anim || touch_active)
|
||||
goto pf_skip;
|
||||
/* the current item's own thumb first (e.g. a
|
||||
* local video opened before its poster was
|
||||
* extracted), then the neighbors': their grid
|
||||
* thumbs are what the slide shows and what
|
||||
* fills the screen the instant a swipe lands.
|
||||
* the full-res prefetch follows. */
|
||||
if (disp_wants_thumb(sel)) {
|
||||
req_issue_thumb(sel);
|
||||
goto pf_skip; /* request slot taken */
|
||||
}
|
||||
int want[2] = { sel + 1, sel - 1 };
|
||||
for (int k = 0; k < 2; k++) {
|
||||
int t = want[k];
|
||||
if (t < 0 || t >= g_disp_count)
|
||||
continue;
|
||||
if (!disp_wants_thumb(t))
|
||||
continue;
|
||||
req_issue_thumb(t);
|
||||
goto pf_skip; /* request slot taken */
|
||||
}
|
||||
for (int k = 0; k < 2; k++) {
|
||||
int t = want[k];
|
||||
if (t < 0 || t >= g_disp_count)
|
||||
continue;
|
||||
if (pf_d[0] == t || pf_d[1] == t)
|
||||
continue;
|
||||
struct disp_item *ti = &g_disp[t];
|
||||
if (ti->src == SRC_LOCAL &&
|
||||
g_local_is_video[ti->idx])
|
||||
continue; /* no poster re-extract */
|
||||
g_req_idx = ti->idx;
|
||||
g_req_src = ti->src;
|
||||
g_req_detail = 1;
|
||||
g_req_is_video = 0;
|
||||
if (ti->src == SRC_LOCAL)
|
||||
snprintf(g_req_path,
|
||||
sizeof(g_req_path), "%s",
|
||||
g_local_path[ti->idx]);
|
||||
else
|
||||
snprintf(g_req_id,
|
||||
sizeof(g_req_id), "%s",
|
||||
g_asset_ids[ti->idx]);
|
||||
__sync_synchronize();
|
||||
g_req_state = REQ_PENDING;
|
||||
pf_pending = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pf_skip:
|
||||
|
||||
vita2d_start_drawing();
|
||||
vita2d_clear_screen();
|
||||
|
||||
if (detail_tex) {
|
||||
draw_texture_zoom(detail_tex, zoom, &panx, &pany);
|
||||
int sliding = (slide_anim ||
|
||||
slide_x > 0.5f || slide_x < -0.5f);
|
||||
if (sliding) {
|
||||
/* current photo offset by the slide, neighbor
|
||||
* riding alongside (its grid thumb blown up;
|
||||
* the full image loads once it lands) */
|
||||
if (detail_tex)
|
||||
draw_texture_fitted(detail_tex, slide_x,
|
||||
0, SCREEN_W, SCREEN_H);
|
||||
else {
|
||||
vita2d_texture *th = disp_thumb(sel);
|
||||
if (th)
|
||||
draw_texture_fitted(th, slide_x, 0,
|
||||
SCREEN_W,
|
||||
SCREEN_H);
|
||||
}
|
||||
int going_next = (slide_x < 0 || slide_goal < 0);
|
||||
int nb = going_next ? sel + 1 : sel - 1;
|
||||
float nx = slide_x +
|
||||
(going_next ? SCREEN_W : -SCREEN_W);
|
||||
if (nb >= 0 && nb < g_disp_count) {
|
||||
/* prefer the prefetched full-res */
|
||||
vita2d_texture *th = NULL;
|
||||
if (pf_d[0] == nb)
|
||||
th = pf_tex[0];
|
||||
else if (pf_d[1] == nb)
|
||||
th = pf_tex[1];
|
||||
if (!th)
|
||||
th = disp_thumb(nb);
|
||||
if (th)
|
||||
draw_texture_fitted(th, nx, 0,
|
||||
SCREEN_W,
|
||||
SCREEN_H);
|
||||
}
|
||||
} else if (is_video) {
|
||||
draw_centered(SCREEN_H / 2,
|
||||
RGBA8(200, 200, 200, 255),
|
||||
"Press X to play this video");
|
||||
/* video: its poster with a play button (the
|
||||
* poster is the detail load for server videos,
|
||||
* the grid thumb for local ones) */
|
||||
vita2d_texture *poster = detail_tex ?
|
||||
detail_tex : disp_thumb(sel);
|
||||
if (poster)
|
||||
draw_texture_fitted(poster, 0, 0,
|
||||
SCREEN_W, SCREEN_H);
|
||||
draw_play_overlay();
|
||||
} else if (detail_tex) {
|
||||
draw_texture_zoom(detail_tex, zoom, &panx, &pany);
|
||||
} else if (detail_loading) {
|
||||
/* low-res grid thumb blown up while the full
|
||||
* image is still on its way */
|
||||
@@ -3366,8 +3823,8 @@ int main(void)
|
||||
"Failed to load this photo");
|
||||
}
|
||||
|
||||
const char *xhint = detail_failed ? "X retry " :
|
||||
is_video ? "X play " : "";
|
||||
const char *xhint = is_video ? "X play " :
|
||||
detail_failed ? "X retry " : "";
|
||||
char hud[200];
|
||||
if (zoom > 1.001f)
|
||||
snprintf(hud, sizeof(hud),
|
||||
@@ -3538,6 +3995,9 @@ int main(void)
|
||||
for (int i = 0; i < g_texpool_n; i++) /* drain the recycle pool */
|
||||
vita2d_free_texture(g_texpool[i]);
|
||||
g_texpool_n = 0;
|
||||
for (int k = 0; k < 2; k++)
|
||||
if (pf_tex[k])
|
||||
vita2d_free_texture(pf_tex[k]);
|
||||
if (detail_tex)
|
||||
vita2d_free_texture(detail_tex);
|
||||
for (int i = 0; i < g_asset_count; i++)
|
||||
|
||||
Reference in New Issue
Block a user