Login screen, account switching, and UI polish

Login: an interactive sign-in screen (server/email/password via the on-screen
keyboard, touch + d-pad, password masked) shown when there's no usable
credential or a config login fails, plus a "Try the demo server" button. The
bundled Immich logo is now the official transparent PNG (clean edges), drawn
minified on a slow spin for the startup loader.

Logout (cloud menu, top-left) returns to the sign-in screen instead of quitting;
on re-auth, reset_for_account_change() pauses the sync thread, drains the
thumbnail worker, recycles thumbnails, drops the old library/local/search state
and reloads from the new account. The thumbnail worker now builds its auth
header (and resolve list) per request, so a new token takes effect immediately
instead of looping on "invalid user token"; g_serverip is cleared on logout.

Grid: Square now drops focus to the grid AND hides the top bar in one press.

Verified against demo.immich.app (v2.7.5) over TLS 1.2: login, grid/search,
cloud page, logout -> Try demo account switch, all with working thumbnails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sads
2026-06-13 18:48:49 +02:00
co-authored by Claude Opus 4.8
parent c5c5fe955b
commit b329cc6b50
2 changed files with 369 additions and 21 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+369 -21
View File
@@ -209,6 +209,12 @@ static char g_srv_total[40]; /* disk size, human ("1.8 TiB") */
static int g_srv_pct; /* disk usage percentage */ static int g_srv_pct; /* disk usage percentage */
static char g_srv_version[40]; /* "v1.119.0" */ static char g_srv_version[40]; /* "v1.119.0" */
/* account switch (logout -> login): pause the sync thread while the main
* thread drops the old library and reloads, so it can't read/realloc the
* arrays mid-reset */
static volatile int g_pause_bg; /* request the sync thread to idle */
static volatile int g_sync_idle; /* sync thread acknowledges it idled */
/* Texture recycling pool. Freeing a texture unmaps its memblock, and the /* Texture recycling pool. Freeing a texture unmaps its memblock, and the
* GPU side (notably Vita3K's texture cache, which re-reads guest memory of * GPU side (notably Vita3K's texture cache, which re-reads guest memory of
* cached textures at its own pace) may still touch it afterwards — freeing * cached textures at its own pace) may still touch it afterwards — freeing
@@ -406,9 +412,11 @@ static void draw_loading(const char *text, unsigned int frame)
if (g_logo) { if (g_logo) {
float lw = vita2d_texture_get_width(g_logo); float lw = vita2d_texture_get_width(g_logo);
float lh = vita2d_texture_get_height(g_logo); float lh = vita2d_texture_get_height(g_logo);
float scale = 110.0f / lw; /* draw smaller than the source so the GPU minifies it (smooth
* edges) and spin slowly */
float scale = 96.0f / lw;
vita2d_draw_texture_scale_rotate_hotspot(g_logo, cx, cy, vita2d_draw_texture_scale_rotate_hotspot(g_logo, cx, cy,
scale, scale, frame * 0.10f, lw / 2.0f, lh / 2.0f); scale, scale, frame * 0.045f, lw / 2.0f, lh / 2.0f);
} }
int w = text_width(1.0f, text); int w = text_width(1.0f, text);
draw_text((SCREEN_W - w) / 2, cy + 90.0f, draw_text((SCREEN_W - w) / 2, cy + 90.0f,
@@ -1345,10 +1353,6 @@ static unsigned char *extract_video_poster(const char *path, int *w, int *h,
static int worker_thread(SceSize args, void *argp) static int worker_thread(SceSize args, void *argp)
{ {
CURL *curl = curl_easy_init(); CURL *curl = curl_easy_init();
char keyhdr[300];
auth_header(keyhdr, sizeof(keyhdr));
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_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
@@ -1434,11 +1438,20 @@ static int worker_thread(SceSize args, void *argp)
g_server, g_req_id); g_server, g_req_id);
curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
/* build auth per request so an account switch (new token)
* takes effect immediately */
char keyhdr[300];
auth_header(keyhdr, sizeof(keyhdr));
struct curl_slist *hdrs = curl_slist_append(NULL, keyhdr);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
curl_easy_setopt(curl, CURLOPT_RESOLVE, g_resolve_list);
net_lock(); /* TLS is single-threaded, see g_net_mutex */ net_lock(); /* TLS is single-threaded, see g_net_mutex */
res = curl_easy_perform(curl); res = curl_easy_perform(curl);
net_unlock(); net_unlock();
if (res == CURLE_OK) if (res == CURLE_OK)
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, NULL);
curl_slist_free_all(hdrs);
} }
if (src == SRC_SERVER && res != CURLE_OK) { if (src == SRC_SERVER && res != CURLE_OK) {
@@ -2065,7 +2078,7 @@ static void draw_sel_outline(float x, float y, float w, float h)
* texture's bilinear sampling are what soften the corners. */ * texture's bilinear sampling are what soften the corners. */
static vita2d_texture *rounded_mask_tex(int w, int h, float r) static vita2d_texture *rounded_mask_tex(int w, int h, float r)
{ {
enum { NSLOT = 8 }; /* a small fixed set of shapes (pill, buttons, ...) */ enum { NSLOT = 14 }; /* a small fixed set of shapes (pill, buttons, ...) */
static vita2d_texture *cache[NSLOT]; static vita2d_texture *cache[NSLOT];
static int cw[NSLOT], ch[NSLOT]; static int cw[NSLOT], ch[NSLOT];
static float cr[NSLOT]; static float cr[NSLOT];
@@ -2498,7 +2511,7 @@ static void utf16_to_utf8(const uint16_t *s, char *out, int outcap)
* driving the dialog each frame; the worker thread keeps running, which is * driving the dialog each frame; the worker thread keeps running, which is
* fine since we touch no shared arrays here. */ * fine since we touch no shared arrays here. */
static int ime_input(const char *title, const char *initial, static int ime_input(const char *title, const char *initial,
char *out, size_t outsz) char *out, size_t outsz, int password)
{ {
static uint16_t title16[SCE_IME_DIALOG_MAX_TITLE_LENGTH]; static uint16_t title16[SCE_IME_DIALOG_MAX_TITLE_LENGTH];
static uint16_t init16[129]; static uint16_t init16[129];
@@ -2514,7 +2527,8 @@ static int ime_input(const char *title, const char *initial,
p.type = SCE_IME_TYPE_DEFAULT; p.type = SCE_IME_TYPE_DEFAULT;
p.option = SCE_IME_OPTION_NO_AUTO_CAPITALIZATION; p.option = SCE_IME_OPTION_NO_AUTO_CAPITALIZATION;
p.dialogMode = SCE_IME_DIALOG_DIALOG_MODE_WITH_CANCEL; p.dialogMode = SCE_IME_DIALOG_DIALOG_MODE_WITH_CANCEL;
p.textBoxMode = SCE_IME_DIALOG_TEXTBOX_MODE_WITH_CLEAR; p.textBoxMode = password ? SCE_IME_DIALOG_TEXTBOX_MODE_PASSWORD :
SCE_IME_DIALOG_TEXTBOX_MODE_WITH_CLEAR;
p.title = title16; p.title = title16;
p.maxTextLength = 128; p.maxTextLength = 128;
p.initialText = init16; p.initialText = init16;
@@ -2548,6 +2562,220 @@ static int ime_input(const char *title, const char *initial,
return confirmed; return confirmed;
} }
static void draw_hud(const char *text); /* defined below */
/* persist server + credentials (and the loader's other settings) to config */
static void save_config(void)
{
FILE *f = fopen(CONFIG_PATH, "w");
if (!f)
return;
fprintf(f, "server=%s\n", g_server);
fprintf(f, "apikey=%s\n", g_apikey);
if (g_email[0]) fprintf(f, "email=%s\n", g_email);
if (g_password[0]) fprintf(f, "password=%s\n", g_password);
if (g_serverip[0]) fprintf(f, "serverip=%s\n", g_serverip);
for (int i = 0; i < g_syncdir_count; i++)
fprintf(f, "syncdir=%s\n", g_syncdirs[i]);
if (g_syncmax_mb != 512)
fprintf(f, "syncmaxmb=%ld\n", g_syncmax_mb);
fclose(f);
}
/* give g_server an https:// scheme if it lacks one, strip a trailing slash */
static void normalize_server(void)
{
size_t n = strlen(g_server);
while (n > 0 && g_server[n - 1] == '/')
g_server[--n] = '\0';
if (g_server[0] && !strstr(g_server, "://")) {
char tmp[512];
snprintf(tmp, sizeof(tmp), "https://%s", g_server);
snprintf(g_server, sizeof(g_server), "%s", tmp);
}
}
/* interactive sign-in: server + email + password via the on-screen keyboard.
* blocks until a login succeeds, then saves the config. */
static void login_screen(void)
{
int sel = 0; /* 0 server, 1 email, 2 password, 3 log in, 4 demo */
char errmsg[160] = "";
unsigned int prev = 0, frame = 0;
/* field / button geometry, shared by the touch hit-test and the drawer */
const float fx = 180, fw = 600, fh = 44, row0 = 200, rowgap = 58;
const float by = row0 + 3 * rowgap + 6; /* Log in button */
const float dy = by + fh + 12; /* Try demo button */
int lt_down = 0, lt_drag = 0;
float lt_x = 0, lt_y = 0, lt_sx = 0, lt_sy = 0;
for (;;) {
SceCtrlData pad;
sceCtrlPeekBufferPositive(0, &pad, 1);
unsigned int pressed = pad.buttons & ~prev;
prev = pad.buttons;
frame++;
if (pressed & SCE_CTRL_UP) sel = (sel + 4) % 5;
if (pressed & SCE_CTRL_DOWN) sel = (sel + 1) % 5;
/* activate an item with X, or by tapping it on the touchscreen */
int activate = (pressed & SCE_CTRL_CROSS) ? sel : -1;
{
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 (!lt_down) {
lt_down = 1;
lt_drag = 0;
lt_sx = tx;
lt_sy = ty;
} else if (fabsf(tx - lt_sx) > 14 ||
fabsf(ty - lt_sy) > 14) {
lt_drag = 1;
}
lt_x = tx;
lt_y = ty;
} else if (lt_down) {
lt_down = 0;
if (!lt_drag && lt_x >= fx && lt_x <= fx + fw) {
for (int i = 0; i < 3; i++) {
float fy = row0 + i * rowgap;
if (lt_y >= fy && lt_y <= fy + fh) {
sel = i;
activate = i;
}
}
if (lt_y >= by && lt_y <= by + fh) {
sel = 3;
activate = 3;
} else if (lt_y >= dy && lt_y <= dy + fh) {
sel = 4;
activate = 4;
}
}
}
}
if (activate >= 0) {
if (activate == 0) {
ime_input("Server URL (https://...)", g_server,
g_server, sizeof(g_server), 0);
normalize_server();
} else if (activate == 1) {
ime_input("Email", g_email, g_email,
sizeof(g_email), 0);
} else if (activate == 2) {
ime_input("Password", g_password, g_password,
sizeof(g_password), 1);
} else {
/* activate 4 = Try demo: prefill the public demo */
if (activate == 4) {
snprintf(g_server, sizeof(g_server),
"https://demo.immich.app");
snprintf(g_email, sizeof(g_email),
"demo@immich.app");
snprintf(g_password, sizeof(g_password),
"demo");
}
g_token[0] = g_apikey[0] = '\0';
normalize_server();
if (!g_server[0] || !g_email[0] || !g_password[0]) {
snprintf(errmsg, sizeof(errmsg),
"Enter server, email and password");
} else {
draw_loading("Signing in...", frame);
char lerr[120];
if (do_login(lerr, sizeof(lerr)) == 0) {
save_config();
return;
}
snprintf(errmsg, sizeof(errmsg),
"Login failed: %.100s", lerr);
}
}
prev = 0xFFFFFFFF; /* swallow buttons held in the dialog */
lt_down = 0;
continue;
}
vita2d_start_drawing();
vita2d_clear_screen();
if (g_logo) {
float lw = vita2d_texture_get_width(g_logo);
float s = 72.0f / lw;
vita2d_draw_texture_scale(g_logo, SCREEN_W / 2.0f - 36.0f,
44.0f, s, s);
}
draw_centered(150, RGBA8(255, 255, 255, 255), "Sign in to Immich");
const char *labels[3] = { "Server", "Email", "Password" };
char pwmask[40];
int pl = (int)strlen(g_password);
if (pl > 24) pl = 24;
memset(pwmask, '*', pl);
pwmask[pl] = '\0';
const char *vals[3];
vals[0] = g_server[0] ? g_server : "(tap to enter)";
vals[1] = g_email[0] ? g_email : "(tap to enter)";
vals[2] = g_password[0] ? pwmask : "(tap to enter)";
for (int i = 0; i < 3; i++) {
float fy = row0 + i * rowgap;
if (sel == i) {
vita2d_texture *r = rounded_mask_tex(
(int)fw + 6, (int)fh + 6, 13.0f);
if (r)
vita2d_draw_texture_tint(r, fx - 3, fy - 3,
RGBA8(94, 110, 215, 255));
}
vita2d_texture *bx = rounded_mask_tex((int)fw, (int)fh, 10.0f);
if (bx)
vita2d_draw_texture_tint(bx, fx, fy,
RGBA8(38, 38, 42, 255));
draw_text(fx + 14, fy + 16, RGBA8(150, 150, 158, 255),
0.75f, labels[i]);
draw_text(fx + 14, fy + 38, RGBA8(230, 230, 235, 255),
0.95f, vals[i]);
}
if (sel == 3) {
vita2d_texture *r = rounded_mask_tex((int)fw + 6,
(int)fh + 6, 13.0f);
if (r)
vita2d_draw_texture_tint(r, fx - 3, by - 3,
RGBA8(120, 140, 235, 255));
}
vita2d_texture *lb = rounded_mask_tex((int)fw, (int)fh, 10.0f);
if (lb)
vita2d_draw_texture_tint(lb, fx, by,
RGBA8(94, 110, 215, 255));
draw_centered((int)by + 30, RGBA8(245, 245, 250, 255), "Log in");
/* Try demo button */
if (sel == 4) {
vita2d_texture *r = rounded_mask_tex((int)fw + 6,
(int)fh + 6, 13.0f);
if (r)
vita2d_draw_texture_tint(r, fx - 3, dy - 3,
RGBA8(120, 140, 235, 255));
}
vita2d_texture *dbn = rounded_mask_tex((int)fw, (int)fh, 10.0f);
if (dbn)
vita2d_draw_texture_tint(dbn, fx, dy, RGBA8(50, 52, 60, 255));
draw_centered((int)dy + 30, RGBA8(210, 210, 218, 255),
"Try the demo server");
if (errmsg[0])
draw_centered(SCREEN_H - 46, RGBA8(220, 120, 120, 255),
errmsg);
draw_hud("Up/Down select X edit / log in");
vita2d_end_drawing();
vita2d_swap_buffers();
sceDisplayWaitVblankStart();
}
}
/* run an Immich smart (CLIP) search and switch the grid to its results. /* run an Immich smart (CLIP) search and switch the grid to its results.
* main thread only: it appends any newly-seen assets to the g_asset_* arrays * main thread only: it appends any newly-seen assets to the g_asset_* arrays
* (like fetch_page) and rebuilds g_disp from the result set. returns the * (like fetch_page) and rebuilds g_disp from the result set. returns the
@@ -3839,6 +4067,14 @@ static int sync_thread(SceSize args, void *argp)
sync_curl_init(); sync_curl_init();
for (;;) { for (;;) {
/* idle while the main thread resets for an account switch */
if (g_pause_bg) {
g_sync_idle = 1;
sceKernelDelayThread(8 * 1000);
continue;
}
g_sync_idle = 0;
/* 1. hash + dup-check a batch of freshly scanned files */ /* 1. hash + dup-check a batch of freshly scanned files */
int batch[100], nb = 0; int batch[100], nb = 0;
for (int j = 0; j < g_local_count && nb < 100; j++) { for (int j = 0; j < g_local_count && nb < 100; j++) {
@@ -4021,6 +4257,62 @@ static int init_load_thread(SceSize args, void *argp)
return 0; return 0;
} }
/* after a logout + sign-in (possibly a different account): drop the old
* library/local lists and reload, with the sync thread paused and the
* thumbnail worker drained so nothing reads/writes the arrays mid-reset.
* main thread only. */
static void reset_for_account_change(void)
{
draw_loading("Switching account...", 0);
/* pause the sync thread (it reads g_local_* directly) */
g_pause_bg = 1;
for (int i = 0; i < 300 && !g_sync_idle; i++)
sceKernelDelayThread(10 * 1000);
/* drain any in-flight thumbnail request so a late REQ_DONE can't write
* g_thumb[] at an index that's about to become invalid */
for (int i = 0; i < 300 && g_req_state == REQ_PENDING; i++)
sceKernelDelayThread(10 * 1000);
if (g_req_state == REQ_DONE) {
free(g_req_pix); g_req_pix = NULL;
free(g_req_raw); g_req_raw = NULL;
}
g_req_state = REQ_IDLE;
g_req_idx = -1;
__sync_synchronize();
/* recycle every thumbnail and drop the old library + local lists */
vita2d_wait_rendering_done();
for (int i = 0; i < g_asset_count; i++)
if (g_thumb[i]) {
tex_release(g_thumb[i]);
g_thumb[i] = NULL;
g_thumb_failed[i] = 0;
}
for (int j = 0; j < g_local_count; j++)
if (g_local_thumb[j]) {
tex_release(g_local_thumb[j]);
g_local_thumb[j] = NULL;
}
g_asset_count = 0;
g_local_count = 0;
g_disp_count = 0;
g_sect_count = 0;
g_next_page = 1;
g_search_active = 0;
g_search_count = 0;
g_srv_state = 0; /* re-fetch the cloud page's server info */
__sync_synchronize();
/* reload from the new account (sync still paused), then resume */
fetch_page(0);
scan_local_media();
grow_locals(g_local_count + 512);
rebuild_display();
g_pause_bg = 0;
}
int main(void) int main(void)
{ {
vita2d_init(); vita2d_init();
@@ -4091,10 +4383,7 @@ int main(void)
log_line("protocols: %s", plist); log_line("protocols: %s", plist);
} }
if (load_config() != 0) int have_cfg = (load_config() == 0);
fatal_error("Edit it with VitaShell, then restart the app. Set "
"server= and either apikey= or email=/password=.",
"Configure your Immich server + login in " CONFIG_PATH);
/* log the exact bytes of the server URL; invisible characters in the /* log the exact bytes of the server URL; invisible characters in the
* config show up here when curl complains about the protocol */ * config show up here when curl complains about the protocol */
@@ -4106,14 +4395,15 @@ int main(void)
log_line("server='%s' len=%d hex=%s", g_server, n, hex); log_line("server='%s' len=%d hex=%s", g_server, n, hex);
} }
/* no API key: log in with email/password to get a bearer token */ /* auth: API key needs nothing; email/password from config is tried once;
if (!g_apikey[0] && g_email[0]) { * anything missing or a failed login drops to the interactive sign-in */
if (!have_cfg) {
login_screen();
} else if (!g_apikey[0] && g_email[0]) {
draw_loading("Signing in...", 0); draw_loading("Signing in...", 0);
char lerr[160]; char lerr[160];
if (do_login(lerr, sizeof(lerr)) != 0) if (do_login(lerr, sizeof(lerr)) != 0)
fatal_error(lerr, login_screen();
"Login failed. Check email/password (or use "
"an apikey) in " CONFIG_PATH);
} }
/* spinning-logo loading screen while the first page loads on a thread */ /* spinning-logo loading screen while the first page loads on a thread */
@@ -4294,8 +4584,12 @@ int main(void)
* move across the elements, down or [] returns to * move across the elements, down or [] returns to
* the grid, X activates the focused element */ * the grid, X activates the focused element */
show_sel = 0; show_sel = 0;
if ((pressed & SCE_CTRL_SQUARE) || if (pressed & SCE_CTRL_SQUARE) {
(nav & SCE_CTRL_DOWN)) { /* drop to the grid AND hide the bar */
bar_focus = 0;
show_sel = 1;
bar_shown = 0;
} else if (nav & SCE_CTRL_DOWN) {
bar_focus = 0; bar_focus = 0;
show_sel = 1; show_sel = 1;
} else { } else {
@@ -4548,7 +4842,7 @@ int main(void)
char q[128] = ""; char q[128] = "";
int ok = ime_input("Search your photos", int ok = ime_input("Search your photos",
g_search_active ? g_search_query : "", g_search_active ? g_search_query : "",
q, sizeof(q)); q, sizeof(q), 0);
if (ok && q[0]) { if (ok && q[0]) {
show_status("Searching \"%s\"...", q); show_status("Searching \"%s\"...", q);
int nres = run_smart_search(q); int nres = run_smart_search(q);
@@ -5209,8 +5503,11 @@ pf_skip:
int do_upload = (pressed & SCE_CTRL_TRIANGLE) != 0; int do_upload = (pressed & SCE_CTRL_TRIANGLE) != 0;
int do_toggle = (pressed & SCE_CTRL_SQUARE) != 0; int do_toggle = (pressed & SCE_CTRL_SQUARE) != 0;
int do_back = (pressed & SCE_CTRL_CIRCLE) != 0; int do_back = (pressed & SCE_CTRL_CIRCLE) != 0;
int do_logout = 0;
/* O / Back button in the top-right corner */ /* O / Back button in the top-right corner */
float bkd = 36, bkcx = SCREEN_W - 30, bkcy = 36; float bkd = 36, bkcx = SCREEN_W - 30, bkcy = 36;
/* Log out button in the top-left corner */
float lox = 16, loy = 18, low = 120, loh = 36;
/* touch: tap the button to open the details list */ /* touch: tap the button to open the details list */
{ {
@@ -5237,6 +5534,11 @@ pf_skip:
} else if (touch_y < bkcy + bkd && } else if (touch_y < bkcy + bkd &&
touch_x > bkcx - bkd - 70) { touch_x > bkcx - bkd - 70) {
do_back = 1; /* top-right Back */ do_back = 1; /* top-right Back */
} else if (touch_x >= lox &&
touch_x <= lox + low &&
touch_y >= loy &&
touch_y <= loy + loh) {
do_logout = 1; /* top-left Log out */
} else if (touch_y >= tgy - 6 && } else if (touch_y >= tgy - 6 &&
touch_y <= tgy + tgh + 6 && touch_y <= tgy + tgh + 6 &&
touch_x >= 60 && touch_x <= tgx + tgw) { touch_x >= 60 && touch_x <= tgx + tgw) {
@@ -5257,6 +5559,40 @@ pf_skip:
mode = MODE_GRID; mode = MODE_GRID;
continue; continue;
} }
if (do_logout) {
/* best-effort server logout, clear credentials,
* persist, then return to the sign-in screen */
membuf lb;
long lc;
char lu[600];
snprintf(lu, sizeof(lu), "%s/api/auth/logout",
g_server);
http_request(lu, "{}", &lb, &lc, NULL, 0);
free(lb.data);
g_token[0] = g_apikey[0] = '\0';
g_email[0] = g_password[0] = '\0';
g_serverip[0] = '\0'; /* drop the old LAN-IP pin */
save_config();
login_screen(); /* blocks until re-signed-in */
/* drop the old account's library + reload */
reset_for_account_change();
sel = 0;
scroll = target = 0;
grid_last_sel = -1;
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;
}
mode = MODE_GRID;
detail_idx = -1;
prev_buttons = 0xFFFFFFFF; /* swallow held keys */
touch_active = 0;
continue;
}
if (do_toggle) { if (do_toggle) {
g_autobackup = !g_autobackup; g_autobackup = !g_autobackup;
save_autobackup(); save_autobackup();
@@ -5289,6 +5625,18 @@ pf_skip:
RGBA8(235, 90, 85, 255), 26); RGBA8(235, 90, 85, 255), 26);
} }
/* top-left Log out button */
{
vita2d_texture *lo = rounded_mask_tex((int)low,
(int)loh, 10.0f);
if (lo)
vita2d_draw_texture_tint(lo, lox, loy,
RGBA8(60, 46, 52, 255));
int w = text_width(0.9f, "Log out");
draw_text(lox + (low - w) / 2, loy + loh - 13,
RGBA8(235, 200, 205, 255), 0.9f, "Log out");
}
char line[300]; char line[300];
uint32_t c = RGBA8(220, 220, 220, 255); uint32_t c = RGBA8(220, 220, 220, 255);
uint32_t dim = RGBA8(150, 150, 155, 255); uint32_t dim = RGBA8(150, 150, 155, 255);