Viewer: save to Vita, turn with L/R; album name order, cover shape setting
- Inside an album, SELECT cycles newest / oldest / file name A-Z / Z-A (natural, case-insensitive; name orders drop the month headers and L/R jump by initial letter). Asset originalFileName is now parsed. - Full-screen viewer: Triangle or the "Save to Vita" button downloads the original (falling back to Immich's full-size / preview JPEG) and exports it to the Photos app via ScePhotoExport (new src/export.c). The video downloader is now a generic download_to_file(). - R / L turn the whole viewer 90 degrees clockwise / counter-clockwise so the Vita can be held upright (new src/viewrot.c): it renders into a portrait render target drawn rotated, and maps d-pad, sticks and touch into the turned screen. Zoom moves to the right stick only. SCREEN_W/H are now variables for that logical screen. - Server & Backup page: album cover shape setting (vertical 3:4, square, panoramic 16:9), saved as covershape= in config.txt. - Test builds: "rup" holds the right stick up in test_input.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+9
-2
@@ -23,7 +23,7 @@
|
||||
|
||||
#define ALB_COLS 4
|
||||
#define ALB_CW 210.0f /* card width */
|
||||
#define ALB_COVH 158.0f /* cover height (4:3) */
|
||||
#define ALB_COVH alb_cover_h() /* cover height: the cover shape */
|
||||
#define ALB_GAP 20.0f
|
||||
#define ALB_ROWH (ALB_COVH + 62.0f) /* cover + two text lines + gap */
|
||||
#define ALB_TOP 64.0f /* below the page header */
|
||||
@@ -38,6 +38,13 @@
|
||||
#define ALB_OX (ALB_SX + ALB_SW + 14.0f) /* sort chip */
|
||||
#define ALB_OW 230.0f
|
||||
|
||||
/* cover height for the chosen shape: 3:4, 1:1 or 16:9 */
|
||||
static float alb_cover_h(void)
|
||||
{
|
||||
return g_cover_shape == COVER_VERTICAL ? 280.0f :
|
||||
g_cover_shape == COVER_WIDE ? 118.0f : 210.0f;
|
||||
}
|
||||
|
||||
enum { ALB_NONE, ALB_BACK, ALB_OPEN, ALB_REFRESH, ALB_SEARCH };
|
||||
|
||||
/* sort orders, cycled by SELECT */
|
||||
@@ -244,7 +251,7 @@ static void draw_album_card(int a, float x, float y, int selected)
|
||||
draw_sel_outline(x - 5, y - 5, ALB_CW + 10, ALB_COVH + 10);
|
||||
vita2d_draw_rectangle(x, y, ALB_CW, ALB_COVH, card);
|
||||
if (tex) {
|
||||
/* crop to fill the 4:3 cover box */
|
||||
/* crop to fill the cover box */
|
||||
float tw = vita2d_texture_get_width(tex);
|
||||
float th = vita2d_texture_get_height(tex);
|
||||
float s = fmaxf(ALB_CW / tw, ALB_COVH / th);
|
||||
|
||||
@@ -86,6 +86,7 @@ static int parse_assets(const char *js, size_t jslen, char *err, size_t errlen)
|
||||
added++;
|
||||
g_asset_ids[cur][0] = '\0';
|
||||
g_asset_dates[cur][0] = '\0';
|
||||
g_asset_names[cur][0] = '\0';
|
||||
g_asset_ratio[cur] = 0.0f;
|
||||
g_asset_rot[cur] = 0;
|
||||
g_asset_is_video[cur] = 0;
|
||||
@@ -136,6 +137,10 @@ static int parse_assets(const char *js, size_t jslen, char *err, size_t errlen)
|
||||
else if (klen == 4 && !strncmp(k, "type", 4))
|
||||
g_asset_is_video[cur] =
|
||||
(vlen == 5 && !strncmp(js + val->start, "VIDEO", 5));
|
||||
else if (klen == 16 && !strncmp(k, "originalFileName", 16))
|
||||
/* may be cut mid UTF-8 sequence; only sorted, never drawn */
|
||||
snprintf(g_asset_names[cur], sizeof(g_asset_names[0]),
|
||||
"%.*s", vlen, js + val->start);
|
||||
}
|
||||
if (cur >= 0 && exw > 0 && exh > 0)
|
||||
g_asset_ratio[cur] = (exori >= 5 && exori <= 8) ?
|
||||
@@ -175,6 +180,8 @@ static int dedup_new_assets(int before)
|
||||
sizeof(g_asset_ids[0]));
|
||||
memcpy(g_asset_dates[i], g_asset_dates[n],
|
||||
sizeof(g_asset_dates[0]));
|
||||
memcpy(g_asset_names[i], g_asset_names[n],
|
||||
sizeof(g_asset_names[0]));
|
||||
g_asset_ratio[i] = g_asset_ratio[n];
|
||||
g_asset_rot[i] = g_asset_rot[n];
|
||||
g_asset_is_video[i] = g_asset_is_video[n];
|
||||
@@ -642,6 +649,8 @@ static int fetch_album_assets(int a)
|
||||
!strcmp(g_asset_ids[j], g_asset_ids[i])) {
|
||||
memcpy(g_asset_dates[j], g_asset_dates[i],
|
||||
sizeof(g_asset_dates[0]));
|
||||
memcpy(g_asset_names[j], g_asset_names[i],
|
||||
sizeof(g_asset_names[0]));
|
||||
g_asset_ratio[j] = g_asset_ratio[i];
|
||||
g_asset_rot[j] = g_asset_rot[i];
|
||||
g_asset_is_video[j] = g_asset_is_video[i];
|
||||
|
||||
@@ -21,6 +21,8 @@ static char g_boot_status[48] = "Loading...";
|
||||
static volatile int g_boot_done;
|
||||
static volatile int g_boot_login_failed;
|
||||
#ifdef VIMM_TEST_INPUT
|
||||
/* a pseudo button: hold the right stick up (zoom in) */
|
||||
#define TEST_RSTICK_UP 0x80000000u
|
||||
/* TEST BUILDS ONLY (-DVIMM_TEST_INPUT): scripted buttons for emulator runs
|
||||
* where synthetic key events can't reach Vita3K. Lines in
|
||||
* ux0:/data/vitaimmich/test_input.txt: "x", "down:3" (repeat), "wait:60"
|
||||
@@ -53,6 +55,7 @@ static unsigned int test_input_buttons(void)
|
||||
{ "left", SCE_CTRL_LEFT }, { "right", SCE_CTRL_RIGHT },
|
||||
{ "l", SCE_CTRL_LTRIGGER }, { "r", SCE_CTRL_RTRIGGER },
|
||||
{ "sel", SCE_CTRL_SELECT }, { "start", SCE_CTRL_START },
|
||||
{ "rup", TEST_RSTICK_UP },
|
||||
};
|
||||
if (!strcmp(name, "wait")) {
|
||||
queue_btn[qn] = 0; queue_len[qn++] = n;
|
||||
@@ -238,6 +241,8 @@ int main(void)
|
||||
/* on-screen keyboard (smart search). the IME runs as a common dialog;
|
||||
* the config tells it the system language + enter/cancel button map */
|
||||
sceSysmoduleLoadModule(SCE_SYSMODULE_IME);
|
||||
/* Save to Vita: copies photos into the Photos app */
|
||||
sceSysmoduleLoadModule(SCE_SYSMODULE_PHOTO_EXPORT);
|
||||
{
|
||||
SceCommonDialogConfigParam cfg;
|
||||
sceCommonDialogConfigParamInit(&cfg);
|
||||
@@ -416,10 +421,15 @@ int main(void)
|
||||
int pf_pending = -1; /* slot being fetched right now, -1 none */
|
||||
|
||||
for (;;) {
|
||||
view_leave(); /* only the photo viewer may be turned */
|
||||
SceCtrlData pad;
|
||||
sceCtrlPeekBufferPositive(0, &pad, 1);
|
||||
#ifdef VIMM_TEST_INPUT
|
||||
pad.buttons |= test_input_buttons();
|
||||
if (pad.buttons & TEST_RSTICK_UP) {
|
||||
pad.buttons &= ~TEST_RSTICK_UP;
|
||||
pad.ry = 0;
|
||||
}
|
||||
#endif
|
||||
unsigned int pressed = pad.buttons & ~prev_buttons;
|
||||
prev_buttons = pad.buttons;
|
||||
@@ -476,6 +486,22 @@ int main(void)
|
||||
if (held_frames > 18 && held_frames % 5 == 0)
|
||||
nav |= pad.buttons & dirs;
|
||||
|
||||
/* SELECT inside an album cycles its order (date / name); the
|
||||
* grid restarts at the top of the new order */
|
||||
if ((pressed & SCE_CTRL_SELECT) && mode == MODE_GRID &&
|
||||
g_album_active) {
|
||||
g_album_sort = (g_album_sort + 1) % ASORT_COUNT;
|
||||
rebuild_display();
|
||||
log_line("album sort: %s (first: %s, %s)",
|
||||
g_album_sort_label[g_album_sort],
|
||||
g_disp_count > 0 ? g_asset_names[g_disp[0].idx] : "-",
|
||||
g_disp_count > 1 ? g_asset_names[g_disp[1].idx] : "-");
|
||||
sel = 0;
|
||||
scroll = target = 0;
|
||||
grid_last_sel = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* SELECT opens the cloud / backup page from the grid or detail */
|
||||
if ((pressed & SCE_CTRL_SELECT) &&
|
||||
(mode == MODE_GRID || mode == MODE_DETAIL)) {
|
||||
@@ -1105,13 +1131,25 @@ int main(void)
|
||||
int is_local = (it.src == SRC_LOCAL);
|
||||
int is_video = disp_is_video(sel);
|
||||
|
||||
/* zoom with the triggers or the right stick (push up to
|
||||
* zoom in, down to zoom out), continuous while held */
|
||||
/* R / L turn the viewer 90 degrees clockwise / counter-
|
||||
* clockwise (hold the Vita upright for portrait photos);
|
||||
* from here on the frame works in the turned screen:
|
||||
* SCREEN_W/H, d-pad, sticks and touch all follow it */
|
||||
if (!slide_anim && !touch_active &&
|
||||
(pressed & (SCE_CTRL_RTRIGGER | SCE_CTRL_LTRIGGER))) {
|
||||
view_turn((pressed & SCE_CTRL_RTRIGGER) ? 90 : -90);
|
||||
zoom = 1.0f;
|
||||
panx = pany = 0.0f;
|
||||
}
|
||||
view_enter();
|
||||
nav = view_buttons(nav);
|
||||
pad.buttons = view_buttons(pad.buttons);
|
||||
view_stick(&pad.lx, &pad.ly);
|
||||
view_stick(&pad.rx, &pad.ry);
|
||||
|
||||
/* zoom with the right stick (push up to zoom in, down
|
||||
* to zoom out), continuous while held */
|
||||
if (!is_video && detail_tex) {
|
||||
if (pad.buttons & SCE_CTRL_RTRIGGER)
|
||||
zoom *= 1.04f;
|
||||
if (pad.buttons & SCE_CTRL_LTRIGGER)
|
||||
zoom /= 1.04f;
|
||||
float rz = (128 - pad.ry) / 128.0f; /* up positive */
|
||||
if (rz > 0.18f || rz < -0.18f)
|
||||
zoom *= 1.0f + rz * 0.05f;
|
||||
@@ -1123,6 +1161,10 @@ int main(void)
|
||||
}
|
||||
}
|
||||
int zoomed = (zoom > 1.001f);
|
||||
/* Triangle (or the button) saves a server photo to the
|
||||
* Photos app; local photos already live on the Vita */
|
||||
int can_save = !is_local && !is_video;
|
||||
int do_save = can_save && (pressed & SCE_CTRL_TRIANGLE);
|
||||
|
||||
if (zoomed) {
|
||||
/* pan with the left stick + d-pad; don't browse */
|
||||
@@ -1159,6 +1201,7 @@ int main(void)
|
||||
if (td.reportNum > 0) {
|
||||
float tx = td.report[0].x * 0.5f;
|
||||
float ty = td.report[0].y * 0.5f;
|
||||
view_touch(&tx, &ty);
|
||||
if (!touch_active) {
|
||||
touch_active = 1;
|
||||
touch_dragged = 0;
|
||||
@@ -1201,9 +1244,14 @@ int main(void)
|
||||
else
|
||||
slide_goal = 0; /* spring back */
|
||||
slide_anim = 1;
|
||||
} else if (!touch_dragged && can_save &&
|
||||
!zoomed && !slide_anim &&
|
||||
save_button_hit(touch_x, touch_y)) {
|
||||
do_save = 1;
|
||||
} else if (!touch_dragged && is_video &&
|
||||
!slide_anim) {
|
||||
/* tap on the poster plays it */
|
||||
view_leave();
|
||||
g_dl_bg = detail_tex ?
|
||||
detail_tex :
|
||||
disp_thumb(sel);
|
||||
@@ -1261,6 +1309,7 @@ int main(void)
|
||||
if (pressed & SCE_CTRL_CROSS) {
|
||||
if (is_video) {
|
||||
/* play even if the poster fetch failed */
|
||||
view_leave();
|
||||
g_dl_bg = detail_tex ?
|
||||
detail_tex : disp_thumb(sel);
|
||||
g_video_rot = 0;
|
||||
@@ -1282,6 +1331,20 @@ int main(void)
|
||||
if (detail_failed)
|
||||
detail_idx = -1; /* retry */
|
||||
}
|
||||
if (do_save && !slide_anim) {
|
||||
view_leave();
|
||||
g_dl_bg = detail_tex ? detail_tex : disp_thumb(sel);
|
||||
save_photo_to_vita(it.idx);
|
||||
g_dl_bg = NULL;
|
||||
/* buttons used on the progress screen (O cancels)
|
||||
* must not also act here */
|
||||
do {
|
||||
sceCtrlPeekBufferPositive(0, &pad, 1);
|
||||
sceKernelDelayThread(10 * 1000);
|
||||
} while (pad.buttons);
|
||||
prev_buttons = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* left/right may have moved the selection; refresh the
|
||||
* item we load + draw below (the X handler above acted
|
||||
@@ -1289,6 +1352,7 @@ int main(void)
|
||||
it = g_disp[sel];
|
||||
is_local = (it.src == SRC_LOCAL);
|
||||
is_video = disp_is_video(sel);
|
||||
can_save = !is_local && !is_video;
|
||||
|
||||
/* collect a finished neighbor prefetch (and drop it
|
||||
* if browsing already moved past its neighborhood) */
|
||||
@@ -1454,8 +1518,7 @@ int main(void)
|
||||
}
|
||||
pf_skip:
|
||||
|
||||
vita2d_start_drawing();
|
||||
vita2d_clear_screen();
|
||||
view_begin_drawing();
|
||||
|
||||
int sliding = (slide_anim ||
|
||||
slide_x > 0.5f || slide_x < -0.5f);
|
||||
@@ -1531,23 +1594,25 @@ pf_skip:
|
||||
char hud[200];
|
||||
if (zoom > 1.001f)
|
||||
snprintf(hud, sizeof(hud),
|
||||
"%d / %d %.0f%% stick/d-pad pan L/R zoom O back",
|
||||
"%d / %d %.0f%% stick/d-pad pan R-stick zoom O back",
|
||||
sel + 1, g_disp_count, zoom * 100.0f);
|
||||
else
|
||||
snprintf(hud, sizeof(hud),
|
||||
"%d / %d %.19s < > browse L/R zoom %sO back",
|
||||
"%d / %d %.19s < > browse L/R turn %sO back",
|
||||
sel + 1, g_disp_count,
|
||||
g_disp_count > 0 ? disp_date(sel) : "",
|
||||
xhint);
|
||||
draw_hud(hud);
|
||||
if (can_save && !zoomed && !sliding)
|
||||
draw_save_button();
|
||||
draw_toast();
|
||||
/* throbber centred over the preview while the full-res
|
||||
* image is still loading */
|
||||
if (detail_loading && disp_thumb(sel) && !zoomed && !sliding)
|
||||
draw_throbber(SCREEN_W / 2.0f, SCREEN_H / 2.0f,
|
||||
24.0f, frame);
|
||||
|
||||
vita2d_end_drawing();
|
||||
vita2d_swap_buffers();
|
||||
view_end_drawing();
|
||||
} else if (mode == MODE_CLOUD) {
|
||||
/* server + backup overview, opened from the cloud
|
||||
* button on the search bar */
|
||||
@@ -1593,6 +1658,14 @@ pf_skip:
|
||||
int do_toggle = (pressed & SCE_CTRL_SQUARE) != 0;
|
||||
int do_back = (pressed & SCE_CTRL_CIRCLE) != 0;
|
||||
int do_logout = 0;
|
||||
/* album cover shape picker (right column): three
|
||||
* segments, tap one or step with L / R */
|
||||
float shx = 584, shy = 306, shw = 120, shh = 40;
|
||||
int new_shape = g_cover_shape;
|
||||
if (pressed & SCE_CTRL_LTRIGGER)
|
||||
new_shape = (new_shape + COVER_COUNT - 1) % COVER_COUNT;
|
||||
if (pressed & SCE_CTRL_RTRIGGER)
|
||||
new_shape = (new_shape + 1) % COVER_COUNT;
|
||||
/* O / Back button in the top-right corner */
|
||||
float bkd = 36, bkcx = SCREEN_W - 30, bkcy = 36;
|
||||
/* Log out button in the top-left corner */
|
||||
@@ -1628,6 +1701,10 @@ pf_skip:
|
||||
touch_y >= loy &&
|
||||
touch_y <= loy + loh) {
|
||||
do_logout = 1; /* top-left Log out */
|
||||
} else if (touch_y >= shy && touch_y <= shy + shh &&
|
||||
touch_x >= shx &&
|
||||
touch_x < shx + COVER_COUNT * shw) {
|
||||
new_shape = (int)((touch_x - shx) / shw);
|
||||
} else if (touch_y >= tgy - 6 &&
|
||||
touch_y <= tgy + tgh + 6 &&
|
||||
touch_x >= 60 && touch_x <= tgx + tgw) {
|
||||
@@ -1690,6 +1767,12 @@ pf_skip:
|
||||
log_line("cloud: auto-upload %s",
|
||||
g_autobackup ? "on" : "off");
|
||||
}
|
||||
if (new_shape != g_cover_shape) {
|
||||
g_cover_shape = new_shape;
|
||||
save_config();
|
||||
log_line("cloud: album covers %s",
|
||||
g_cover_shape_key[g_cover_shape]);
|
||||
}
|
||||
if (do_upload) {
|
||||
int q = queue_all_uploads();
|
||||
log_line("cloud: queued %d uploads", q);
|
||||
@@ -1793,6 +1876,39 @@ pf_skip:
|
||||
draw_ps_button(tgx - 23, tgy + tgh / 2, 38, ICON_SQUARE,
|
||||
RGBA8(232, 120, 175, 255), 31);
|
||||
|
||||
/* album cover shape: segmented control, the chosen
|
||||
* shape highlighted, each with a small shape preview */
|
||||
draw_text(shx, shy - 14, RGBA8(255, 255, 255, 255), 1.0f,
|
||||
"Album covers");
|
||||
for (int k = 0; k < COVER_COUNT; k++) {
|
||||
float sx = shx + k * shw;
|
||||
int on = (k == g_cover_shape);
|
||||
vita2d_texture *seg = rounded_mask_tex((int)shw - 6,
|
||||
(int)shh, 10.0f);
|
||||
uint32_t sc = on ? RGBA8(94, 110, 215, 255)
|
||||
: RGBA8(46, 46, 52, 255);
|
||||
if (seg)
|
||||
vita2d_draw_texture_tint(seg, sx, shy, sc);
|
||||
else
|
||||
vita2d_draw_rectangle(sx, shy, shw - 6, shh, sc);
|
||||
/* the shape itself, 20 px tall at most */
|
||||
float pw = k == COVER_VERTICAL ? 15 :
|
||||
k == COVER_SQUARE ? 20 : 26;
|
||||
float ph = k == COVER_WIDE ? 15 : 20;
|
||||
vita2d_draw_rectangle(sx + 8, shy + (shh - ph) / 2,
|
||||
pw, ph,
|
||||
on ? RGBA8(235, 235, 245, 255)
|
||||
: RGBA8(130, 130, 138, 255));
|
||||
draw_text(sx + 38, shy + shh - 14,
|
||||
on ? RGBA8(245, 245, 250, 255) : dim, 0.7f,
|
||||
g_cover_shape_label[k]);
|
||||
}
|
||||
{
|
||||
const char *h = "L / R";
|
||||
draw_text(shx + COVER_COUNT * shw - 6 -
|
||||
text_width(0.75f, h), shy - 14, dim, 0.75f, h);
|
||||
}
|
||||
|
||||
/* See details + Upload all buttons */
|
||||
vita2d_texture *db = rounded_mask_tex((int)dbw, (int)bth, 10.0f);
|
||||
if (db)
|
||||
|
||||
@@ -86,6 +86,12 @@ static int load_config(void)
|
||||
}
|
||||
} else if (!strncmp(s, "syncmaxmb=", 10))
|
||||
g_syncmax_mb = atol(clean_line(s + 10));
|
||||
else if (!strncmp(s, "covershape=", 11)) {
|
||||
const char *v = clean_line(s + 11);
|
||||
for (int k = 0; k < COVER_COUNT; k++)
|
||||
if (!strcmp(v, g_cover_shape_key[k]))
|
||||
g_cover_shape = k;
|
||||
}
|
||||
if (!eolsave)
|
||||
break;
|
||||
lp = eol + 1;
|
||||
|
||||
+94
-16
@@ -65,6 +65,74 @@ static int disp_cmp(const void *a, const void *b)
|
||||
return strcmp(dy, dx); /* newest first */
|
||||
}
|
||||
|
||||
/* the open album, sorted by name: one flat grid without month headers */
|
||||
static int layout_flat(void)
|
||||
{
|
||||
return g_album_active && g_album_sort >= ASORT_NAME_AZ;
|
||||
}
|
||||
|
||||
static int disp_cmp_old(const void *a, const void *b)
|
||||
{
|
||||
return -disp_cmp(a, b); /* oldest first */
|
||||
}
|
||||
|
||||
/* case-insensitive "natural" order: digit runs compare by value, so
|
||||
* IMG_2 sorts before IMG_10 */
|
||||
static int name_cmp(const char *a, const char *b)
|
||||
{
|
||||
while (*a && *b) {
|
||||
if (isdigit((unsigned char)*a) && isdigit((unsigned char)*b)) {
|
||||
while (*a == '0')
|
||||
a++;
|
||||
while (*b == '0')
|
||||
b++;
|
||||
const char *ea = a, *eb = b;
|
||||
while (isdigit((unsigned char)*ea))
|
||||
ea++;
|
||||
while (isdigit((unsigned char)*eb))
|
||||
eb++;
|
||||
if (ea - a != eb - b)
|
||||
return (int)(ea - a) - (int)(eb - b);
|
||||
int r = strncmp(a, b, ea - a);
|
||||
if (r)
|
||||
return r;
|
||||
a = ea;
|
||||
b = eb;
|
||||
continue;
|
||||
}
|
||||
int ca = tolower((unsigned char)*a), cb = tolower((unsigned char)*b);
|
||||
if (ca != cb)
|
||||
return ca - cb;
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
return (unsigned char)*a - (unsigned char)*b;
|
||||
}
|
||||
|
||||
static int disp_cmp_name(const void *a, const void *b)
|
||||
{
|
||||
const struct disp_item *x = a, *y = b;
|
||||
int r = name_cmp(g_asset_names[x->idx], g_asset_names[y->idx]);
|
||||
return r ? r : disp_cmp(a, b);
|
||||
}
|
||||
|
||||
static int disp_cmp_name_desc(const void *a, const void *b)
|
||||
{
|
||||
return -disp_cmp_name(a, b);
|
||||
}
|
||||
|
||||
/* group of a slot: its "YYYY-MM" month, or in a name-sorted album the
|
||||
* file name's first letter (what L/R jumps between) */
|
||||
static void disp_group(int d, char *out, size_t len)
|
||||
{
|
||||
if (layout_flat()) {
|
||||
const char *n = g_asset_names[g_disp[d].idx];
|
||||
snprintf(out, len, "%c", toupper((unsigned char)n[0]));
|
||||
} else {
|
||||
snprintf(out, len, "%.7s", disp_date(d));
|
||||
}
|
||||
}
|
||||
|
||||
/* turn a sortable "YYYY-MM-..." date into a "Month YYYY" section title */
|
||||
static void month_label(const char *date, char *out, size_t len)
|
||||
{
|
||||
@@ -133,12 +201,13 @@ static void layout_grid(void)
|
||||
int row_start = 0;
|
||||
char curkey[8] = "";
|
||||
|
||||
int flat = layout_flat();
|
||||
for (int d = 0; d < g_disp_count; d++) {
|
||||
const char *date = disp_date(d);
|
||||
char key[8];
|
||||
snprintf(key, sizeof(key), "%.7s", date); /* YYYY-MM */
|
||||
snprintf(key, sizeof(key), "%.7s", flat ? "" : date); /* YYYY-MM */
|
||||
|
||||
if (strcmp(key, curkey) != 0) {
|
||||
if (!flat && strcmp(key, curkey) != 0) {
|
||||
if (d > row_start) /* month's partial last row */
|
||||
layout_finish_row(row_start, d, &y, natw, 0);
|
||||
row_start = d;
|
||||
@@ -200,7 +269,8 @@ static void rebuild_display(void)
|
||||
}
|
||||
|
||||
/* album mode: the open album's members (server assets), date-grouped
|
||||
* like search results; placeholders without a date are skipped */
|
||||
* like search results or in a flat name order (g_album_sort);
|
||||
* placeholders without a date are skipped */
|
||||
if (g_album_active) {
|
||||
if (!grow_disp(g_album_idx_count > 0 ? g_album_idx_count : 1))
|
||||
return;
|
||||
@@ -214,7 +284,12 @@ static void rebuild_display(void)
|
||||
n++;
|
||||
}
|
||||
g_disp_count = n;
|
||||
qsort(g_disp, n, sizeof(g_disp[0]), disp_cmp);
|
||||
int (*cmp)(const void *, const void *) =
|
||||
g_album_sort == ASORT_DATE_OLD ? disp_cmp_old :
|
||||
g_album_sort == ASORT_NAME_AZ ? disp_cmp_name :
|
||||
g_album_sort == ASORT_NAME_ZA ? disp_cmp_name_desc :
|
||||
disp_cmp;
|
||||
qsort(g_disp, n, sizeof(g_disp[0]), cmp);
|
||||
layout_grid();
|
||||
return;
|
||||
}
|
||||
@@ -297,9 +372,9 @@ static void rebuild_keep_view(int *sel, float *scroll, float *target)
|
||||
*sel = 0;
|
||||
}
|
||||
|
||||
/* first item of the month-run after (dir>0, older) or before (dir<0, newer)
|
||||
* the one containing display slot d; the timeline is "YYYY-MM"-grouped and
|
||||
* date-descending, so a month is a contiguous run of equal 7-char prefixes */
|
||||
/* first item of the group-run after (dir>0) or before (dir<0) the one
|
||||
* containing display slot d; the grid is sorted, so a month (or, in a
|
||||
* name-sorted album, an initial letter) is a contiguous run (disp_group) */
|
||||
static int month_jump(int d, int dir)
|
||||
{
|
||||
if (g_disp_count == 0)
|
||||
@@ -308,25 +383,28 @@ static int month_jump(int d, int dir)
|
||||
d = 0;
|
||||
if (d >= g_disp_count)
|
||||
d = g_disp_count - 1;
|
||||
const char *cur = disp_date(d);
|
||||
char cur[8], k[8];
|
||||
disp_group(d, cur, sizeof(cur));
|
||||
int i = d;
|
||||
if (dir > 0) {
|
||||
while (i < g_disp_count - 1) {
|
||||
i++;
|
||||
if (strncmp(disp_date(i), cur, 7))
|
||||
return i; /* first item of the next month */
|
||||
disp_group(i, k, sizeof(k));
|
||||
if (strcmp(k, cur))
|
||||
return i; /* first item of the next group */
|
||||
}
|
||||
return g_disp_count - 1; /* already in the last month */
|
||||
return g_disp_count - 1; /* already in the last group */
|
||||
}
|
||||
/* back to the start of the current month-run */
|
||||
while (i > 0 && !strncmp(disp_date(i - 1), cur, 7))
|
||||
/* back to the start of the current run */
|
||||
while (i > 0 && (disp_group(i - 1, k, sizeof(k)), !strcmp(k, cur)))
|
||||
i--;
|
||||
if (i == 0)
|
||||
return 0;
|
||||
/* then to the start of the previous (newer) month-run */
|
||||
const char *prev = disp_date(i - 1);
|
||||
/* then to the start of the previous run */
|
||||
char prev[8];
|
||||
disp_group(i - 1, prev, sizeof(prev));
|
||||
i--;
|
||||
while (i > 0 && !strncmp(disp_date(i - 1), prev, 7))
|
||||
while (i > 0 && (disp_group(i - 1, k, sizeof(k)), !strcmp(k, prev)))
|
||||
i--;
|
||||
return i;
|
||||
}
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* save a server photo to the Vita's Photos app */
|
||||
/* */
|
||||
/* Triangle in the full-screen viewer. The original is downloaded and */
|
||||
/* handed to ScePhotoExport, which copies it into ux0:picture and */
|
||||
/* registers it with the Photos app. The Vita only takes JPEG/PNG, so */
|
||||
/* an original in another format (HEIC, RAW, WebP...) — or one the */
|
||||
/* exporter rejects — falls back to Immich's full-size JPEG, then to */
|
||||
/* the preview. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define EXPORT_TMP_PATH CONFIG_DIR "/export.tmp"
|
||||
#define EXPORT_WORK_SIZE (4 * 1024 * 1024)
|
||||
|
||||
/* a short status line drawn over the viewer for a few seconds */
|
||||
static char g_toast[200];
|
||||
static uint64_t g_toast_until;
|
||||
|
||||
static void show_toast(const char *text)
|
||||
{
|
||||
snprintf(g_toast, sizeof(g_toast), "%s", text);
|
||||
g_toast_until = sceKernelGetProcessTimeWide() + 3 * 1000 * 1000;
|
||||
}
|
||||
|
||||
static void draw_toast(void)
|
||||
{
|
||||
if (!g_toast[0] || sceKernelGetProcessTimeWide() > g_toast_until)
|
||||
return;
|
||||
int w = text_width(0.9f, g_toast) + 40;
|
||||
float x = (SCREEN_W - w) / 2.0f, y = SCREEN_H - 78;
|
||||
vita2d_texture *pill = rounded_mask_tex(w, 40, 12.0f);
|
||||
if (pill)
|
||||
vita2d_draw_texture_tint(pill, x, y, RGBA8(20, 20, 24, 225));
|
||||
else
|
||||
vita2d_draw_rectangle(x, y, w, 40, RGBA8(20, 20, 24, 225));
|
||||
draw_text(x + 20, y + 27, RGBA8(240, 240, 245, 255), 0.9f, g_toast);
|
||||
}
|
||||
|
||||
/* extension of a JPEG/PNG file by its magic bytes, NULL for anything else */
|
||||
static const char *image_ext(const char *path)
|
||||
{
|
||||
unsigned char m[4] = { 0 };
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f)
|
||||
return NULL;
|
||||
size_t n = fread(m, 1, sizeof(m), f);
|
||||
fclose(f);
|
||||
if (n >= 3 && m[0] == 0xFF && m[1] == 0xD8 && m[2] == 0xFF)
|
||||
return "jpg";
|
||||
if (n >= 4 && !memcmp(m, "\x89PNG", 4))
|
||||
return "png";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* UTF-8 file name (extension dropped) as the UTF-32 photo title */
|
||||
static void export_title(const char *name, SceWChar32 *out, int cap)
|
||||
{
|
||||
const char *dot = strrchr(name, '.');
|
||||
const char *end = dot && dot != name ? dot : name + strlen(name);
|
||||
int n = 0;
|
||||
while (name < end && n < cap - 1) {
|
||||
unsigned char c = (unsigned char)*name;
|
||||
unsigned int cp = '?';
|
||||
int len = 1;
|
||||
if (c < 0x80)
|
||||
cp = c;
|
||||
else if ((c >> 5) == 0x6 && name + 1 < end)
|
||||
cp = ((c & 0x1F) << 6) | (name[1] & 0x3F), len = 2;
|
||||
else if ((c >> 4) == 0xE && name + 2 < end)
|
||||
cp = ((c & 0x0F) << 12) | ((name[1] & 0x3F) << 6) |
|
||||
(name[2] & 0x3F), len = 3;
|
||||
else if ((c >> 3) == 0x1E && name + 3 < end)
|
||||
cp = ((c & 0x07) << 18) | ((name[1] & 0x3F) << 12) |
|
||||
((name[2] & 0x3F) << 6) | (name[3] & 0x3F), len = 4;
|
||||
out[n++] = cp;
|
||||
name += len;
|
||||
}
|
||||
out[n] = 0;
|
||||
}
|
||||
|
||||
/* save server asset idx to the Photos app; reports through the toast */
|
||||
static void save_photo_to_vita(int idx)
|
||||
{
|
||||
if (g_asset_is_video[idx]) {
|
||||
show_toast("Only photos can be saved to the Vita");
|
||||
return;
|
||||
}
|
||||
static void *work;
|
||||
if (!work)
|
||||
work = memalign(64, EXPORT_WORK_SIZE);
|
||||
if (!work) {
|
||||
show_toast("Not enough memory to save the photo");
|
||||
return;
|
||||
}
|
||||
|
||||
static const char *const sources[] = {
|
||||
"original", "thumbnail?size=fullsize", "thumbnail?size=preview",
|
||||
};
|
||||
static SceWChar32 title[64];
|
||||
export_title(g_asset_names[idx][0] ? g_asset_names[idx] : "Immich",
|
||||
title, 64);
|
||||
g_dl_what = "photo";
|
||||
char err[160] = "", file[96], out[256];
|
||||
int saved = 0;
|
||||
for (unsigned s = 0; s < sizeof(sources) / sizeof(sources[0]) && !saved; s++) {
|
||||
char url[700];
|
||||
snprintf(url, sizeof(url), "%s/api/assets/%s/%s", g_server,
|
||||
g_asset_ids[idx], sources[s]);
|
||||
int r = download_to_file(url, EXPORT_TMP_PATH, err, sizeof(err));
|
||||
if (r == 1) {
|
||||
show_toast("Save cancelled");
|
||||
return;
|
||||
}
|
||||
if (r < 0)
|
||||
continue;
|
||||
const char *ext = image_ext(EXPORT_TMP_PATH);
|
||||
if (!ext) {
|
||||
log_line("export %s: %s is not JPEG/PNG", g_asset_ids[idx],
|
||||
sources[s]);
|
||||
snprintf(err, sizeof(err), "unsupported image format");
|
||||
sceIoRemove(EXPORT_TMP_PATH);
|
||||
continue;
|
||||
}
|
||||
/* the exporter goes by the extension */
|
||||
snprintf(file, sizeof(file), CONFIG_DIR "/export.%s", ext);
|
||||
sceIoRemove(file);
|
||||
sceIoRename(EXPORT_TMP_PATH, file);
|
||||
|
||||
/* the SDK doesn't document `version`; try the firmware's 3.15
|
||||
* value, then 0, and log each result for diagnosis */
|
||||
static const int versions[] = { 0x03150021, 0 };
|
||||
int ret = -1;
|
||||
for (unsigned v = 0; v < 2 && ret < 0; v++) {
|
||||
PhotoExportParam param;
|
||||
memset(¶m, 0, sizeof(param));
|
||||
param.version = versions[v];
|
||||
param.photoTitle = title;
|
||||
out[0] = '\0';
|
||||
ret = scePhotoExportFromFile(file, ¶m, work, NULL, NULL,
|
||||
out, sizeof(out));
|
||||
log_line("export %s (%s, v%08X): 0x%08X -> %s",
|
||||
g_asset_ids[idx], sources[s], versions[v], ret,
|
||||
out);
|
||||
}
|
||||
sceIoRemove(file);
|
||||
if (ret >= 0)
|
||||
saved = 1;
|
||||
else
|
||||
snprintf(err, sizeof(err), "export error 0x%08X", ret);
|
||||
}
|
||||
if (saved) {
|
||||
show_toast("Saved to the Photos app");
|
||||
} else {
|
||||
char msg[200];
|
||||
snprintf(msg, sizeof(msg), "Could not save the photo (%s)", err);
|
||||
show_toast(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/* the viewer's "Save to Vita" button (bottom right; Triangle or a tap) */
|
||||
#define SAVE_BTN_W 176.0f
|
||||
#define SAVE_BTN_H 40.0f
|
||||
#define SAVE_BTN_X (SCREEN_W - SAVE_BTN_W - 16.0f)
|
||||
#define SAVE_BTN_Y (SCREEN_H - SAVE_BTN_H - 16.0f)
|
||||
|
||||
static void draw_save_button(void)
|
||||
{
|
||||
vita2d_texture *bg = rounded_mask_tex((int)SAVE_BTN_W, (int)SAVE_BTN_H, 12.0f);
|
||||
if (bg)
|
||||
vita2d_draw_texture_tint(bg, SAVE_BTN_X, SAVE_BTN_Y,
|
||||
RGBA8(20, 20, 24, 200));
|
||||
draw_ps_button(SAVE_BTN_X + 22, SAVE_BTN_Y + SAVE_BTN_H / 2, 28, ICON_TRI,
|
||||
RGBA8(90, 200, 170, 255), 18);
|
||||
draw_text(SAVE_BTN_X + 44, SAVE_BTN_Y + 27, RGBA8(235, 235, 240, 255),
|
||||
0.85f, "Save to Vita");
|
||||
}
|
||||
|
||||
static int save_button_hit(float x, float y)
|
||||
{
|
||||
return x >= SAVE_BTN_X && x < SAVE_BTN_X + SAVE_BTN_W &&
|
||||
y >= SAVE_BTN_Y && y < SAVE_BTN_Y + SAVE_BTN_H;
|
||||
}
|
||||
+9
-2
@@ -42,6 +42,7 @@
|
||||
#include <psp2/net/netctl.h>
|
||||
#include <psp2/sysmodule.h>
|
||||
#include <psp2/ime_dialog.h>
|
||||
#include <psp2/photoexport.h>
|
||||
#include <psp2/common_dialog.h>
|
||||
|
||||
#include <vita2d.h>
|
||||
@@ -54,8 +55,11 @@
|
||||
#define JSMN_PARENT_LINKS
|
||||
#include "jsmn.h"
|
||||
|
||||
#define SCREEN_W 960
|
||||
#define SCREEN_H 544
|
||||
/* the logical screen the UI lays itself out in: 960x544, except while the
|
||||
* rotated photo viewer draws into its portrait render target (544x960) */
|
||||
static int g_screen_w = 960, g_screen_h = 544;
|
||||
#define SCREEN_W g_screen_w
|
||||
#define SCREEN_H g_screen_h
|
||||
|
||||
#define COLS 5 /* rough items per row, for prefetch distances only */
|
||||
#define ROW_H 200 /* justified-grid row height; widths follow aspect */
|
||||
@@ -124,6 +128,7 @@ static vita2d_texture *g_ic_both;
|
||||
* 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 char (*g_asset_names)[64]; /* originalFileName, "" = unknown */
|
||||
static float *g_asset_ratio; /* display w/h from exif, 0 = unknown */
|
||||
static unsigned char *g_asset_rot; /* raw exif orientation (0 = none) */
|
||||
static unsigned char *g_asset_is_video;
|
||||
@@ -162,5 +167,7 @@ static char (*g_tex_err)[160];
|
||||
#include "map.c" /* geotag map: markers, clustering, projection */
|
||||
#include "albums.c" /* album list page + album open/fetch threads */
|
||||
#include "video.c" /* video download + SceAvPlayer playback */
|
||||
#include "export.c" /* save a photo to the Vita's Photos app */
|
||||
#include "viewrot.c" /* L/R-turned photo viewer (render target) */
|
||||
#include "sync.c" /* local scan/hash/check/upload + sync overview */
|
||||
#include "app.c" /* main(): init, event loop, threads */
|
||||
|
||||
@@ -373,6 +373,7 @@ static int map_open_marker(int marker_idx, int *out_sel)
|
||||
idx = g_asset_count++;
|
||||
snprintf(g_asset_ids[idx], sizeof(g_asset_ids[0]), "%s", id);
|
||||
snprintf(g_asset_dates[idx], sizeof(g_asset_dates[0]), "%s", dval);
|
||||
g_asset_names[idx][0] = '\0';
|
||||
g_asset_ratio[idx] = 0.0f; /* unknown: detail view fits anyway */
|
||||
g_asset_rot[idx] = 0;
|
||||
g_asset_is_video[idx] = !strncmp(tval, "VIDEO", 5);
|
||||
@@ -722,6 +723,7 @@ static int asset_for_id(const char *id)
|
||||
int idx = g_asset_count++;
|
||||
snprintf(g_asset_ids[idx], sizeof(g_asset_ids[0]), "%s", id);
|
||||
g_asset_dates[idx][0] = '\0'; /* "" = unresolved; meta_worker fills it */
|
||||
g_asset_names[idx][0] = '\0';
|
||||
g_asset_ratio[idx] = 0.0f;
|
||||
g_asset_rot[idx] = 0;
|
||||
g_asset_is_video[idx] = 0;
|
||||
|
||||
+18
@@ -149,6 +149,23 @@ 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 */
|
||||
/* order of the open album's grid, cycled by SELECT: date newest / oldest
|
||||
* (month headers) or file name A-Z / Z-A (one flat grid, no headers) */
|
||||
enum { ASORT_DATE_NEW, ASORT_DATE_OLD, ASORT_NAME_AZ, ASORT_NAME_ZA, ASORT_COUNT };
|
||||
static int g_album_sort;
|
||||
static const char *const g_album_sort_label[ASORT_COUNT] = {
|
||||
"Newest first", "Oldest first", "Name A-Z", "Name Z-A",
|
||||
};
|
||||
/* shape of the album page's cover thumbnails (setting on the Server &
|
||||
* Backup page, saved as covershape= in config.txt) */
|
||||
enum { COVER_VERTICAL, COVER_SQUARE, COVER_WIDE, COVER_COUNT };
|
||||
static int g_cover_shape = COVER_SQUARE;
|
||||
static const char *const g_cover_shape_key[COVER_COUNT] = {
|
||||
"vertical", "square", "wide",
|
||||
};
|
||||
static const char *const g_cover_shape_label[COVER_COUNT] = {
|
||||
"Vertical", "Square", "Panoramic",
|
||||
};
|
||||
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;
|
||||
@@ -220,6 +237,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_names, c, need) ||
|
||||
!GROW(g_asset_ratio, c, need) ||
|
||||
!GROW(g_asset_rot, c, need) ||
|
||||
!GROW(g_asset_is_video, c, need) ||
|
||||
|
||||
@@ -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 = 20 }; /* a small fixed set of shapes (pill, buttons, ...) */
|
||||
enum { NSLOT = 32 }; /* a small fixed set of shapes (pill, buttons, ...) */
|
||||
static vita2d_texture *cache[NSLOT];
|
||||
static int cw[NSLOT], ch[NSLOT];
|
||||
static float cr[NSLOT];
|
||||
@@ -473,11 +473,17 @@ static void draw_search_bar(float yoff, int focus)
|
||||
|
||||
float tx = BAR_PX + 44, ty = by + BAR_H - 13;
|
||||
if (g_album_active && g_album_open >= 0 && g_album_open < g_album_len) {
|
||||
/* inside an album: its name, and the O chip to go back */
|
||||
/* inside an album: its name, the order (SELECT cycles it) and
|
||||
* the O chip to go back */
|
||||
char q[80];
|
||||
snprintf(q, sizeof(q), "%.48s", g_album_name[g_album_open][0] ?
|
||||
snprintf(q, sizeof(q), "%.40s", g_album_name[g_album_open][0] ?
|
||||
g_album_name[g_album_open] : "(untitled album)");
|
||||
draw_text(tx, ty, RGBA8(235, 235, 240, 255), 0.95f, q);
|
||||
char srt[40];
|
||||
snprintf(srt, sizeof(srt), "SELECT %s",
|
||||
g_album_sort_label[g_album_sort]);
|
||||
draw_text(BAR_CLEAR_CX - 24 - text_width(0.8f, srt), ty,
|
||||
RGBA8(140, 140, 148, 255), 0.8f, srt);
|
||||
int gs = 26;
|
||||
vita2d_texture *og = icon_tex(ICON_CIRCLE, gs);
|
||||
if (og)
|
||||
@@ -622,6 +628,8 @@ static void save_config(void)
|
||||
fprintf(f, "syncdir=%s\n", g_syncdirs[i]);
|
||||
if (g_syncmax_mb != 512)
|
||||
fprintf(f, "syncmaxmb=%ld\n", g_syncmax_mb);
|
||||
if (g_cover_shape != COVER_SQUARE)
|
||||
fprintf(f, "covershape=%s\n", g_cover_shape_key[g_cover_shape]);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
|
||||
+31
-19
@@ -52,6 +52,8 @@ static size_t file_write_cb(void *ptr, size_t size, size_t nmemb, void *ud)
|
||||
}
|
||||
|
||||
static uint64_t g_dl_last_draw;
|
||||
/* what the progress screen says is downloading ("video", "photo") */
|
||||
static const char *g_dl_what = "video";
|
||||
/* 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 */
|
||||
@@ -121,11 +123,12 @@ static int dl_progress_cb(void *ud, curl_off_t dltotal, curl_off_t dlnow,
|
||||
|
||||
char buf[128];
|
||||
if (dltotal > 0)
|
||||
snprintf(buf, sizeof(buf), "Downloading video... %.1f / %.1f MB",
|
||||
dlnow / (1024.0 * 1024.0), dltotal / (1024.0 * 1024.0));
|
||||
snprintf(buf, sizeof(buf), "Downloading %s... %.1f / %.1f MB",
|
||||
g_dl_what, dlnow / (1024.0 * 1024.0),
|
||||
dltotal / (1024.0 * 1024.0));
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "Downloading video... %.1f MB",
|
||||
dlnow / (1024.0 * 1024.0));
|
||||
snprintf(buf, sizeof(buf), "Downloading %s... %.1f MB",
|
||||
g_dl_what, dlnow / (1024.0 * 1024.0));
|
||||
|
||||
vita2d_start_drawing();
|
||||
vita2d_clear_screen();
|
||||
@@ -149,17 +152,15 @@ static int dl_progress_cb(void *ud, curl_off_t dltotal, curl_off_t dlnow,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* download the playback stream of asset idx to VIDEO_TMP_PATH.
|
||||
* returns 0 on success, 1 if the user cancelled, -1 on error. */
|
||||
static int download_video(int idx, char *err, size_t errlen)
|
||||
/* download `url` (an authenticated server path) to the file `path`, with
|
||||
* the progress screen (O cancels). returns 0 on success, 1 if the user
|
||||
* cancelled, -1 on error (`err` filled); the file is removed unless 0. */
|
||||
static int download_to_file(const char *url, const char *path, char *err,
|
||||
size_t errlen)
|
||||
{
|
||||
char url[700];
|
||||
snprintf(url, sizeof(url), "%s/api/assets/%s/video/playback",
|
||||
g_server, g_asset_ids[idx]);
|
||||
|
||||
FILE *f = fopen(VIDEO_TMP_PATH, "wb");
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) {
|
||||
snprintf(err, errlen, "cannot create %s", VIDEO_TMP_PATH);
|
||||
snprintf(err, errlen, "cannot create %s", path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -180,7 +181,7 @@ static int download_video(int idx, char *err, size_t errlen)
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, f);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||
/* no overall timeout (videos can be big); abort on a 30 s stall */
|
||||
/* no overall timeout (files can be big); abort on a 30 s stall */
|
||||
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L);
|
||||
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 30L);
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
||||
@@ -200,30 +201,41 @@ static int download_video(int idx, char *err, size_t errlen)
|
||||
if (res == CURLE_OK)
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
|
||||
curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD_T, &dl);
|
||||
log_line("video dl %s: res=%d code=%ld bytes=%lld",
|
||||
g_asset_ids[idx], res, code, (long long)dl);
|
||||
log_line("%s dl %s: res=%d code=%ld bytes=%lld", g_dl_what, url, res,
|
||||
code, (long long)dl);
|
||||
curl_slist_free_all(hdrs);
|
||||
curl_easy_cleanup(curl);
|
||||
fclose(f);
|
||||
|
||||
if (res == CURLE_ABORTED_BY_CALLBACK) {
|
||||
sceIoRemove(VIDEO_TMP_PATH);
|
||||
sceIoRemove(path);
|
||||
return 1;
|
||||
}
|
||||
if (res != CURLE_OK) {
|
||||
snprintf(err, errlen, "curl error %d: %s", res,
|
||||
curl_easy_strerror(res));
|
||||
sceIoRemove(VIDEO_TMP_PATH);
|
||||
sceIoRemove(path);
|
||||
return -1;
|
||||
}
|
||||
if (code < 200 || code >= 300) {
|
||||
snprintf(err, errlen, "HTTP %ld", code);
|
||||
sceIoRemove(VIDEO_TMP_PATH);
|
||||
sceIoRemove(path);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* download the playback stream of asset idx to VIDEO_TMP_PATH.
|
||||
* returns 0 on success, 1 if the user cancelled, -1 on error. */
|
||||
static int download_video(int idx, char *err, size_t errlen)
|
||||
{
|
||||
char url[700];
|
||||
snprintf(url, sizeof(url), "%s/api/assets/%s/video/playback",
|
||||
g_server, g_asset_ids[idx]);
|
||||
g_dl_what = "video";
|
||||
return download_to_file(url, VIDEO_TMP_PATH, err, errlen);
|
||||
}
|
||||
|
||||
static void *av_alloc(void *p, uint32_t alignment, uint32_t size)
|
||||
{
|
||||
void *r = memalign(alignment, size);
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* turned photo viewer */
|
||||
/* */
|
||||
/* L / R in the full-screen viewer turn the whole viewer (not the */
|
||||
/* photo) 90 degrees, so the Vita can be held upright and portrait */
|
||||
/* photos fill the screen. While turned, the viewer lays itself out in */
|
||||
/* its logical screen (SCREEN_W x SCREEN_H = 544x960 when upright), */
|
||||
/* draws into an off-screen render target of that size, and the target */
|
||||
/* is drawn rotated onto the real 960x544 screen. Buttons, sticks and */
|
||||
/* touch are mapped into the logical screen so "up" stays up for the */
|
||||
/* person holding the Vita. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* vita2d keeps its projection in this global and uploads it with every
|
||||
* draw; it is set once for 960x544, so render targets of another size need
|
||||
* it swapped while they're drawn into */
|
||||
extern float _vita2d_ortho_matrix[4 * 4];
|
||||
void matrix_init_orthographic(float *m, float left, float right, float bottom,
|
||||
float top, float near, float far);
|
||||
|
||||
static int g_view_rot; /* 0 / 90 / 180 / 270, degrees clockwise */
|
||||
static float g_view_rot_anim; /* degrees still to turn (eases to 0) */
|
||||
static vita2d_texture *g_view_rt[2]; /* [0] 960x544, [1] 544x960 */
|
||||
static int g_view_rt_on; /* this frame draws into a render target */
|
||||
|
||||
static void view_ortho(int w, int h)
|
||||
{
|
||||
matrix_init_orthographic(_vita2d_ortho_matrix, 0.0f, w, h, 0.0f,
|
||||
0.0f, 1.0f);
|
||||
}
|
||||
|
||||
/* turn the viewer by +-90 degrees (R: clockwise, L: counter-clockwise) */
|
||||
static void view_turn(int deg)
|
||||
{
|
||||
g_view_rot = (g_view_rot + deg + 360) % 360;
|
||||
g_view_rot_anim -= deg; /* start from the old angle, ease in */
|
||||
log_line("viewer turned to %d", g_view_rot);
|
||||
}
|
||||
|
||||
/* the viewer's logical screen for this frame */
|
||||
static void view_enter(void)
|
||||
{
|
||||
int upright = (g_view_rot % 180) != 0;
|
||||
g_screen_w = upright ? 544 : 960;
|
||||
g_screen_h = upright ? 960 : 544;
|
||||
}
|
||||
|
||||
/* back to the real screen (before anything that draws on its own, like
|
||||
* the video player or a download progress screen) */
|
||||
static void view_leave(void)
|
||||
{
|
||||
g_screen_w = 960;
|
||||
g_screen_h = 544;
|
||||
}
|
||||
|
||||
/* a screen-space direction as seen in the turned viewer */
|
||||
static void view_vec(float sx, float sy, float *lx, float *ly)
|
||||
{
|
||||
switch (g_view_rot) {
|
||||
case 90: *lx = sy; *ly = -sx; break;
|
||||
case 180: *lx = -sx; *ly = -sy; break;
|
||||
case 270: *lx = -sy; *ly = sx; break;
|
||||
default: *lx = sx; *ly = sy; break;
|
||||
}
|
||||
}
|
||||
|
||||
/* d-pad bits as seen in the turned viewer */
|
||||
static unsigned int view_buttons(unsigned int b)
|
||||
{
|
||||
static const struct { unsigned int bit; int x, y; } d[4] = {
|
||||
{ SCE_CTRL_UP, 0, -1 }, { SCE_CTRL_DOWN, 0, 1 },
|
||||
{ SCE_CTRL_LEFT, -1, 0 }, { SCE_CTRL_RIGHT, 1, 0 },
|
||||
};
|
||||
unsigned int out = b & ~(SCE_CTRL_UP | SCE_CTRL_DOWN |
|
||||
SCE_CTRL_LEFT | SCE_CTRL_RIGHT);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (!(b & d[i].bit))
|
||||
continue;
|
||||
float lx, ly;
|
||||
view_vec(d[i].x, d[i].y, &lx, &ly);
|
||||
for (int j = 0; j < 4; j++)
|
||||
if (d[j].x == (int)lx && d[j].y == (int)ly)
|
||||
out |= d[j].bit;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* a stick (0..255, centre 128) as seen in the turned viewer */
|
||||
static void view_stick(unsigned char *x, unsigned char *y)
|
||||
{
|
||||
float lx, ly;
|
||||
view_vec(*x - 128.0f, *y - 128.0f, &lx, &ly);
|
||||
lx += 128.0f;
|
||||
ly += 128.0f;
|
||||
*x = lx < 0 ? 0 : lx > 255 ? 255 : (unsigned char)lx;
|
||||
*y = ly < 0 ? 0 : ly > 255 ? 255 : (unsigned char)ly;
|
||||
}
|
||||
|
||||
/* a touch point on the real screen -> the turned viewer's screen */
|
||||
static void view_touch(float *x, float *y)
|
||||
{
|
||||
float sx = *x, sy = *y;
|
||||
switch (g_view_rot) {
|
||||
case 90: *x = sy; *y = 960 - sx; break;
|
||||
case 180: *x = 960 - sx; *y = 544 - sy; break;
|
||||
case 270: *x = 544 - sy; *y = sx; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
/* start the viewer's frame: straight to the screen when not turned,
|
||||
* else into the render target of the logical screen's size */
|
||||
static void view_begin_drawing(void)
|
||||
{
|
||||
g_view_rt_on = 0;
|
||||
if (g_view_rot != 0 || g_view_rot_anim != 0.0f) {
|
||||
int k = (g_view_rot % 180) != 0;
|
||||
if (!g_view_rt[k])
|
||||
g_view_rt[k] = vita2d_create_empty_texture_rendertarget(
|
||||
SCREEN_W, SCREEN_H, SCE_GXM_TEXTURE_FORMAT_A8B8G8R8);
|
||||
g_view_rt_on = g_view_rt[k] != NULL;
|
||||
if (g_view_rt_on) {
|
||||
/* vita2d_start_drawing() is what empties the per-frame
|
||||
* vertex pool; the _advanced variant doesn't, so without
|
||||
* this every turned frame leaks pool space until a draw
|
||||
* gets NULL and crashes */
|
||||
vita2d_pool_reset();
|
||||
vita2d_start_drawing_advanced(g_view_rt[k],
|
||||
SCE_GXM_SCENE_FRAGMENT_SET_DEPENDENCY);
|
||||
vita2d_clear_screen();
|
||||
view_ortho(SCREEN_W, SCREEN_H);
|
||||
return;
|
||||
}
|
||||
}
|
||||
vita2d_start_drawing();
|
||||
vita2d_clear_screen();
|
||||
}
|
||||
|
||||
/* finish the viewer's frame: draw the render target turned (and scaled
|
||||
* to fit while the turn animates) onto the screen, then swap */
|
||||
static void view_end_drawing(void)
|
||||
{
|
||||
vita2d_end_drawing();
|
||||
if (g_view_rt_on) {
|
||||
vita2d_texture *rt = g_view_rt[(g_view_rot % 180) != 0];
|
||||
float lw = SCREEN_W, lh = SCREEN_H;
|
||||
view_leave();
|
||||
view_ortho(SCREEN_W, SCREEN_H);
|
||||
vita2d_start_drawing_advanced(NULL,
|
||||
SCE_GXM_SCENE_VERTEX_WAIT_FOR_DEPENDENCY);
|
||||
vita2d_clear_screen();
|
||||
float rad = (g_view_rot + g_view_rot_anim) * 3.14159265f / 180.0f;
|
||||
float c = fabsf(cosf(rad)), s = fabsf(sinf(rad));
|
||||
float bw = lw * c + lh * s, bh = lw * s + lh * c;
|
||||
float k = fminf(1.0f, fminf(SCREEN_W / bw, SCREEN_H / bh));
|
||||
vita2d_draw_texture_scale_rotate(rt, SCREEN_W / 2.0f,
|
||||
SCREEN_H / 2.0f, k, k, rad);
|
||||
vita2d_end_drawing();
|
||||
}
|
||||
#ifdef VIMM_TEST_INPUT
|
||||
static unsigned int n;
|
||||
if (++n % 120 == 0)
|
||||
log_line("viewer pool free: %u", vita2d_pool_free_space());
|
||||
#endif
|
||||
vita2d_swap_buffers();
|
||||
g_view_rot_anim *= 0.7f;
|
||||
if (fabsf(g_view_rot_anim) < 0.5f)
|
||||
g_view_rot_anim = 0.0f;
|
||||
}
|
||||
Reference in New Issue
Block a user