From b329cc6b501ad0b4060c9bcf2956d0bb4bf03264 Mon Sep 17 00:00:00 2001 From: Sads Date: Sat, 13 Jun 2026 18:48:49 +0200 Subject: [PATCH] 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 --- assets/immich_logo.png | Bin 8744 -> 11313 bytes src/main.c | 390 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 369 insertions(+), 21 deletions(-) diff --git a/assets/immich_logo.png b/assets/immich_logo.png index f44930f284c8cd18d8ede8cc4a08b5f237bd2f3c..9da6d01354eb0f6f5c9683f6503ebc2a58402438 100644 GIT binary patch literal 11313 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 SAzdQ?0000 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 * 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 @@ -406,9 +412,11 @@ static void draw_loading(const char *text, unsigned int frame) if (g_logo) { float lw = vita2d_texture_get_width(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, - 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); 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) { 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_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); @@ -1434,11 +1438,20 @@ static int worker_thread(SceSize args, void *argp) g_server, g_req_id); curl_easy_setopt(curl, CURLOPT_URL, url); 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 */ res = curl_easy_perform(curl); net_unlock(); if (res == CURLE_OK) 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) { @@ -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. */ 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 int cw[NSLOT], ch[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 * fine since we touch no shared arrays here. */ 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 init16[129]; @@ -2514,7 +2527,8 @@ static int ime_input(const char *title, const char *initial, p.type = SCE_IME_TYPE_DEFAULT; p.option = SCE_IME_OPTION_NO_AUTO_CAPITALIZATION; 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.maxTextLength = 128; p.initialText = init16; @@ -2548,6 +2562,220 @@ static int ime_input(const char *title, const char *initial, 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. * 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 @@ -3839,6 +4067,14 @@ static int sync_thread(SceSize args, void *argp) sync_curl_init(); 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 */ int batch[100], nb = 0; 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; } +/* 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) { vita2d_init(); @@ -4091,10 +4383,7 @@ int main(void) log_line("protocols: %s", plist); } - if (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); + int have_cfg = (load_config() == 0); /* log the exact bytes of the server URL; invisible characters in the * 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); } - /* no API key: log in with email/password to get a bearer token */ - if (!g_apikey[0] && g_email[0]) { + /* auth: API key needs nothing; email/password from config is tried once; + * 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); char lerr[160]; if (do_login(lerr, sizeof(lerr)) != 0) - fatal_error(lerr, - "Login failed. Check email/password (or use " - "an apikey) in " CONFIG_PATH); + login_screen(); } /* 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 * the grid, X activates the focused element */ show_sel = 0; - if ((pressed & SCE_CTRL_SQUARE) || - (nav & SCE_CTRL_DOWN)) { + if (pressed & SCE_CTRL_SQUARE) { + /* 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; show_sel = 1; } else { @@ -4548,7 +4842,7 @@ int main(void) char q[128] = ""; int ok = ime_input("Search your photos", g_search_active ? g_search_query : "", - q, sizeof(q)); + q, sizeof(q), 0); if (ok && q[0]) { show_status("Searching \"%s\"...", q); int nres = run_smart_search(q); @@ -5209,8 +5503,11 @@ pf_skip: int do_upload = (pressed & SCE_CTRL_TRIANGLE) != 0; int do_toggle = (pressed & SCE_CTRL_SQUARE) != 0; int do_back = (pressed & SCE_CTRL_CIRCLE) != 0; + int do_logout = 0; /* 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 */ + float lox = 16, loy = 18, low = 120, loh = 36; /* touch: tap the button to open the details list */ { @@ -5237,6 +5534,11 @@ pf_skip: } else if (touch_y < bkcy + bkd && touch_x > bkcx - bkd - 70) { 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 && touch_y <= tgy + tgh + 6 && touch_x >= 60 && touch_x <= tgx + tgw) { @@ -5257,6 +5559,40 @@ pf_skip: mode = MODE_GRID; 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) { g_autobackup = !g_autobackup; save_autobackup(); @@ -5289,6 +5625,18 @@ pf_skip: 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]; uint32_t c = RGBA8(220, 220, 220, 255); uint32_t dim = RGBA8(150, 150, 155, 255);